2025-11-28 16:27:52 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="default-main">
|
|
|
|
|
<TableHeader :showSearch="false" v-show="flag">
|
|
|
|
|
<template v-slot:select>
|
|
|
|
|
<el-form-item label="日期">
|
|
|
|
|
<DatePicker
|
|
|
|
|
ref="datePickerRef"
|
|
|
|
|
:nextFlag="false"
|
|
|
|
|
:theCurrentTime="true"
|
|
|
|
|
@change="handleDatePickerChange"
|
|
|
|
|
></DatePicker>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-slot:operation>
|
|
|
|
|
<el-button type="primary" icon="el-icon-Edit" @click="editd">编辑</el-button>
|
|
|
|
|
<!-- <el-button type="primary" icon="el-icon-Tools" @click="settings">设置</el-button> -->
|
|
|
|
|
</template>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
|
|
|
|
<GridLayout
|
|
|
|
|
v-model:layout="layout"
|
|
|
|
|
:row-height="rowHeight"
|
|
|
|
|
:is-resizable="false"
|
|
|
|
|
:is-draggable="false"
|
|
|
|
|
:responsive="false"
|
|
|
|
|
:vertical-compact="false"
|
|
|
|
|
prevent-collision
|
|
|
|
|
:col-num="12"
|
|
|
|
|
>
|
|
|
|
|
<template #item="{ item }">
|
|
|
|
|
<div class="box">
|
|
|
|
|
<div class="title">
|
|
|
|
|
<div style="display: flex; align-items: center">
|
|
|
|
|
<Icon class="HelpFilled" :name="(item as LayoutItem).icon" />
|
|
|
|
|
{{ (item as LayoutItem).name }}
|
|
|
|
|
</div>
|
|
|
|
|
<!-- <FullScreen class="HelpFilled" style="cursor: pointer" @click="zoom(item)" /> -->
|
|
|
|
|
<img :src="flag ? img : img1" style="cursor: pointer; height: 16px" @click="zoom(item)" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<component
|
|
|
|
|
:is="(item as LayoutItem).component"
|
|
|
|
|
v-if="(item as LayoutItem).component"
|
|
|
|
|
class="pd10"
|
|
|
|
|
:key="key"
|
|
|
|
|
:timeValue="datePickerRef?.timeValue || 3"
|
|
|
|
|
:height="rowHeight * item.h - seRowHeight(item.h) + 'px'"
|
|
|
|
|
:width="rowWidth * item.w - 30 + 'px'"
|
|
|
|
|
:timeKey="(item as LayoutItem).timeKey"
|
|
|
|
|
:interval="datePickerRef?.interval"
|
|
|
|
|
:w="item.w"
|
|
|
|
|
:h="item.h"
|
|
|
|
|
/>
|
|
|
|
|
<div v-else class="pd10">组件加载失败...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</GridLayout>
|
|
|
|
|
<!-- 设置 -->
|
|
|
|
|
<RoutingConfig ref="RoutingConfigRef" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, onMounted, markRaw, onUnmounted, computed, defineAsyncComponent, type Component } from 'vue'
|
|
|
|
|
import TableHeader from '@/components/table/header/index.vue'
|
|
|
|
|
import { GridLayout } from 'grid-layout-plus'
|
|
|
|
|
import DatePicker from '@/components/form/datePicker/index.vue'
|
|
|
|
|
import { useDebounceFn } from '@vueuse/core'
|
|
|
|
|
import { queryActivatePage, queryByPagePath } from '@/api/system-boot/csstatisticalset'
|
|
|
|
|
import RoutingConfig from '@/views/pqs/cockpit/homePage/components/routingConfig.vue'
|
|
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
|
|
import { useTimeCacheStore } from '@/stores/timeCache'
|
|
|
|
|
const { push } = useRouter()
|
|
|
|
|
const datePickerRef = ref()
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const timeCacheStore = useTimeCacheStore()
|
|
|
|
|
|
|
|
|
|
defineOptions({
|
|
|
|
|
// name: 'dashboard/index'
|
|
|
|
|
})
|
|
|
|
|
// 定义类型
|
|
|
|
|
interface LayoutItem {
|
|
|
|
|
x: number
|
|
|
|
|
y: number
|
|
|
|
|
w: number
|
|
|
|
|
h: number
|
|
|
|
|
timeKey: number | string
|
|
|
|
|
i: string | number
|
|
|
|
|
name: string
|
|
|
|
|
path: string
|
|
|
|
|
icon?: string // 新增 icon 可选字段
|
|
|
|
|
component?: Component | string
|
|
|
|
|
loading?: boolean
|
|
|
|
|
error?: any
|
|
|
|
|
}
|
|
|
|
|
const RoutingConfigRef = ref()
|
|
|
|
|
const key = ref(0)
|
|
|
|
|
const img = new URL(`@/assets/img/amplify.png`, import.meta.url).href
|
|
|
|
|
const img1 = new URL(`@/assets/img/reduce.png`, import.meta.url).href
|
|
|
|
|
// 响应式数据
|
|
|
|
|
const rowHeight = ref(0)
|
|
|
|
|
const rowWidth = ref(0)
|
|
|
|
|
const layout: any = ref([
|
|
|
|
|
// {
|
|
|
|
|
// x: 4,
|
|
|
|
|
// y: 0,
|
|
|
|
|
// w: 4,
|
|
|
|
|
// h: 2,
|
|
|
|
|
// i: '1',
|
|
|
|
|
// name: '',
|
|
|
|
|
// path: '/src/views/pqs/runManage/assessment/components/uese/index.vue'
|
|
|
|
|
// },
|
|
|
|
|
])
|
|
|
|
|
const layoutCopy: any = ref([])
|
|
|
|
|
const flag = ref(true)
|
|
|
|
|
// 组件映射
|
|
|
|
|
const componentMap = reactive(new Map<string, Component | string>())
|
|
|
|
|
const dataList: any = ref({})
|
|
|
|
|
// 获取主内容区域高度
|
|
|
|
|
const getMainHeight = () => {
|
|
|
|
|
const elMain = document.querySelector('.el-main')
|
|
|
|
|
return (elMain?.offsetHeight || 0) - 70
|
|
|
|
|
}
|
|
|
|
|
// 获取主内容区域高度
|
|
|
|
|
const getMainWidth = () => {
|
|
|
|
|
const elMain = document.querySelector('.el-main')
|
|
|
|
|
return (elMain?.offsetWidth || 0) - 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化行高
|
|
|
|
|
const initRowHeight = () => {
|
|
|
|
|
rowHeight.value = Math.max(0, (getMainHeight() - 77 + (flag.value ? 0 : 56)) / 6)
|
|
|
|
|
rowWidth.value = Math.max(0, getMainWidth() / 12)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 动态注册组件
|
|
|
|
|
const registerComponent = (path: string): Component | string | null => {
|
|
|
|
|
if (!path) return null
|
|
|
|
|
|
|
|
|
|
const cacheKey = path
|
|
|
|
|
|
|
|
|
|
// 使用缓存的组件
|
|
|
|
|
if (componentMap.has(cacheKey)) {
|
|
|
|
|
return componentMap.get(cacheKey)!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 动态导入组件
|
|
|
|
|
const modules = import.meta.glob('@/**/*.vue')
|
|
|
|
|
if (!modules[path]) {
|
|
|
|
|
console.error(`组件加载失败: ${path}`)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AsyncComponent = defineAsyncComponent({
|
|
|
|
|
loader: modules[path],
|
|
|
|
|
loadingComponent: () => h('div', '加载中...'),
|
|
|
|
|
errorComponent: ({ error }) => h('div', `加载错误: ${error.message}`),
|
|
|
|
|
delay: 200,
|
|
|
|
|
timeout: 10000
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 保存到映射中
|
|
|
|
|
componentMap.set(cacheKey, markRaw(AsyncComponent))
|
|
|
|
|
return AsyncComponent
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('注册组件失败:', error)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 缩放
|
|
|
|
|
const zoom = (value: any) => {
|
|
|
|
|
if (flag.value) {
|
|
|
|
|
layout.value = [{ ...value, x: 0, y: 0, w: 12, h: 6 }]
|
|
|
|
|
} else {
|
|
|
|
|
layout.value = layoutCopy.value.map((item: any, index: number) => ({
|
|
|
|
|
...item,
|
|
|
|
|
i: item.i || index, // 确保有唯一标识
|
|
|
|
|
component: registerComponent(item.path)
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
console.log('🚀 ~ zoom ~ layout.value:', layout.value)
|
|
|
|
|
|
|
|
|
|
flag.value = !flag.value
|
|
|
|
|
|
|
|
|
|
initRowHeight()
|
|
|
|
|
key.value += 1
|
|
|
|
|
}
|
|
|
|
|
// 计算组件高度
|
|
|
|
|
const seRowHeight = (value: any) => {
|
|
|
|
|
if (value == 6) return 0
|
|
|
|
|
if (value == 5) return 12
|
|
|
|
|
if (value == 4) return 20
|
|
|
|
|
if (value == 3) return 30
|
|
|
|
|
if (value == 2) return 40
|
|
|
|
|
if (value == 1) return 50
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取布局数据
|
|
|
|
|
const fetchLayoutData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
// const { data } = await queryByPagePath({ pagePath: router.currentRoute.value.name })
|
|
|
|
|
// dataList.value = data
|
|
|
|
|
dataList.value = {
|
|
|
|
|
createBy: 'e6a67ccbe789493687766c4304fcb228',
|
|
|
|
|
createTime: '2025-11-26 08:43:56',
|
|
|
|
|
updateBy: 'e6a67ccbe789493687766c4304fcb228',
|
|
|
|
|
updateTime: '2025-11-26 08:43:56',
|
|
|
|
|
id: 'e8218545cc11e32d5d6b87c92fdd0fbc',
|
|
|
|
|
pageName: '测试',
|
|
|
|
|
thumbnail: '',
|
|
|
|
|
remark: '',
|
|
|
|
|
containerConfig:
|
|
|
|
|
'[{"x":0,"y":0,"w":6,"h":3,"i":0.9274640143002512,"name":"终端在线率","path":"/src/components/cockpit/onlineRate/index.vue","icon":"local-审计列表","timeKey":"3","moved":false},{"x":6,"y":0,"w":6,"h":3,"i":0.9154814002445102,"name":"监测点数据完整性","path":"/src/components/cockpit/integrity/index.vue","icon":"local-告警中心","timeKey":"3","moved":false},{"x":0,"y":3,"w":6,"h":3,"i":0.6560899767923003,"name":"终端运行评价","path":"/src/components/cockpit/terminalEvaluation/index.vue","icon":"local-稳态指标超标明细","timeKey":"3","moved":false},{"x":6,"y":3,"w":6,"h":3,"i":0.5812302648025226,"name":"异常数据清洗","path":"/src/components/cockpit/dataCleaning/index.vue","icon":"local-区域暂态评估","timeKey":"3","moved":false}]',
|
|
|
|
|
sort: 100,
|
|
|
|
|
state: 1,
|
|
|
|
|
pagePath: 'dashboard/index6',
|
|
|
|
|
pathName: null,
|
|
|
|
|
routeName: '/src/views/pqs/cockpit/homePage/index.vue',
|
|
|
|
|
icon: ''
|
|
|
|
|
}
|
|
|
|
|
const parsedLayout = JSON.parse(dataList.value.containerConfig || '[]') as LayoutItem[]
|
|
|
|
|
// 处理布局数据
|
|
|
|
|
layout.value = parsedLayout.map((item, index) => ({
|
|
|
|
|
...item,
|
|
|
|
|
i: item.i || index, // 确保有唯一标识
|
|
|
|
|
component: registerComponent(item.path)
|
|
|
|
|
}))
|
|
|
|
|
layoutCopy.value = JSON.parse(JSON.stringify(layout.value))
|
|
|
|
|
initRowHeight()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取布局数据失败:', error)
|
|
|
|
|
// 可以添加错误提示逻辑
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 窗口大小变化处理 - 使用防抖
|
|
|
|
|
const handleResize = useDebounceFn(() => {
|
|
|
|
|
initRowHeight()
|
|
|
|
|
// key.value += 1
|
|
|
|
|
}, 200)
|
|
|
|
|
|
|
|
|
|
// 修改
|
|
|
|
|
const editd = (e: any) => {
|
|
|
|
|
if (dataList.value?.id) {
|
|
|
|
|
push(`/admin/cockpit/popup?id=${dataList.value?.id}&&path=${String(router.currentRoute.value.name)}`)
|
|
|
|
|
} else {
|
|
|
|
|
push(`/admin/cockpit/popup?path=${String(router.currentRoute.value.name)}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 设置
|
|
|
|
|
const settings = () => {
|
|
|
|
|
RoutingConfigRef.value.open()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理 DatePicker 值变化事件
|
|
|
|
|
const handleDatePickerChange = (value: any) => {
|
|
|
|
|
// 将值缓存到 timeCache
|
|
|
|
|
if (value) {
|
|
|
|
|
timeCacheStore.setCache(route.path, value.interval, value.timeValue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生命周期钩子
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// initRowHeight()
|
|
|
|
|
fetchLayoutData()
|
|
|
|
|
|
|
|
|
|
// 添加窗口大小变化监听器
|
|
|
|
|
window.addEventListener('resize', handleResize)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
// 移除监听器防止内存泄漏
|
|
|
|
|
window.removeEventListener('resize', handleResize)
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.vgl-layout {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.vgl-item:not(.vgl-item--placeholder)) {
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.vgl-item:hover:not(.vgl-item--placeholder)) {
|
|
|
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.vgl-item--static) {
|
|
|
|
|
background-color: #f0f2f5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #606266;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.vgl-item) {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.box {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
.title {
|
|
|
|
|
border-bottom: 1px solid #000;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
padding: 0px 10px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
background-color: var(--el-color-primary);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
.HelpFilled {
|
|
|
|
|
height: 16px;
|
|
|
|
|
width: 16px;
|
|
|
|
|
color: #fff !important;
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|