Files
admin-govern/src/views/pqs/cockpit/setUp/components/popup.vue
guanj 676bb37bbe 驾驶舱页面绘制
绘制2、稳态电能质量分析、稳态治理效果分析、暂态电能质量分析页面
2025-10-20 13:25:30 +08:00

470 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="pd10">
<el-card>
<el-form ref="formRef" inline :rules="rules" :model="form" label-width="120px" class="form-four">
<el-form-item label="页面名称" prop="pageName">
<el-input
maxlength="32"
show-word-limit
v-model.trim="form.pageName"
placeholder="请输入页面名称"
></el-input>
</el-form-item>
<el-form-item label="页面排序" prop="sort">
<el-input
maxlength="32"
show-word-limit-number
v-model.trim.number="form.sort"
:min="0"
:step="1"
step-strictly
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" class="top">
<el-input
maxlength="300"
show-word-limit
type="textarea"
:rows="1"
placeholder="请输入内容"
v-model.trim="form.remark"
></el-input>
</el-form-item>
<el-form-item>
<div style="width: 100%; display: flex; justify-content: end">
<el-button type="primary" icon="el-icon-Check" @click="onSubmit">保存</el-button>
<back-component />
</div>
</el-form-item>
</el-form>
</el-card>
<el-card class="mt10" :style="height">
<div style="display: flex">
<div style="width: 605px; overflow: auto" :style="indicatorHeight" class="mr10">
<el-collapse v-model="activeNames" :expand-icon-position="position">
<el-collapse-item
v-for="item in treeComponents"
:key="item.id"
:title="item.name"
:name="item.id"
>
<el-collapse v-model="activeNames1" class="ml20">
<el-collapse-item v-for="k in item.children" :key="k.id" :title="k.name" :name="k.id">
<div class="Box">
<div
v-for="(s, index) in k.children"
:key="index"
class="mr10 mb10 imgBox"
draggable="true"
unselectable="on"
@drag="drag(s)"
@dragend="dragEnd(s)"
>
<div class="textName">{{ s.name }}</div>
<img :src="s.image" style="width: 180px" />
</div>
</div>
</el-collapse-item>
</el-collapse>
</el-collapse-item>
</el-collapse>
</div>
<div style="flex: 1" ref="wrapper">
<GridLayout
class="GridLayout"
ref="gridLayout"
v-model:layout="layout"
style="width: 100%"
:style="{ height: GridHeight + 'px' }"
:row-height="rowHeight"
:col-num="12"
prevent-collision
:vertical-compact="false"
>
<template #item="{ item }">
<div class="imgBox">
<div class="textName">{{ item.name }}</div>
<img
:src="getImg(item.path)"
:style="{
height:
item.h * rowHeight -
(item.h == 1
? 30
: item.h == 2
? 20
: item.h == 3
? 10
: item.h == 4
? -0
: item.h == 5
? -10
: -20) +
'px'
}"
/>
<CloseBold class="remove" @click="removeItem(item.i)" />
</div>
<!-- <span class="text">{{ `${item?.name}` }}</span>
-->
</template>
</GridLayout>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import BackComponent from '@/components/icon/back/index.vue'
import { mainHeight } from '@/utils/layout'
import type { CollapseIconPositionType } from 'element-plus'
import { componentTree } from '@/api/user-boot/user'
import { GridLayout, GridItem } from 'grid-layout-plus'
import { throttle } from 'lodash-es'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Tools, CloseBold } from '@element-plus/icons-vue'
import { addDashboard, updateDashboard, queryById } from '@/api/system-boot/csstatisticalset'
import html2canvas from 'html2canvas'
import { useRoute } from 'vue-router'
// defineOptions({
// name: 'cockpit/popup'
// })
const { go } = useRouter()
const { query } = useRoute()
const height = mainHeight(108)
const indicatorHeight = mainHeight(128)
const rowHeight = ref(0)
const GridHeight = ref(0)
const position = ref<CollapseIconPositionType>('left')
const form: any = reactive({
pageName: '',
thumbnail: '',
containerConfig: [],
sort: '100',
id: '',
remark: ''
})
const activeNames = ref([])
const activeNames1 = ref([])
const rules = {
pageName: [{ required: true, message: '请输入页面名称', trigger: 'blur' }],
projectIds: [{ required: true, message: '请选择工程页面', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }]
}
const formRef = ref()
const layout: any = ref([
// { x: 0, y: 0, w: 4, h: 2, i: '0', name: '', path: '' },
// { x: 4, y: 0, w: 4, h: 2, i: '1', name: '', path: '' },
// { x: 8, y: 0, w: 4, h: 2, i: '2', name: '', path: '' },
// { x: 0, y: 0, w: 4, h: 2, i: '3', name: '', path: '' },
// { x: 4, y: 0, w: 4, h: 2, i: '4', name: '', path: '' },
// { x: 8, y: 0, w: 4, h: 2, i: '5', name: '', path: '' },
// { x: 0, y: 0, w: 4, h: 2, i: '6', name: '', path: '' },
// { x: 4, y: 0, w: 4, h: 2, i: '7', name: '', path: '' },
// { x: 8, y: 0, w: 4, h: 2, i: '8', name: '', path: '' }
])
const treeComponents: any = ref([]) //组件树
const treeComponentsCopy: any = ref([]) //组件树
const info = () => {
activeNames.value = []
activeNames1.value = []
componentTree().then(res => {
treeComponents.value = res.data
activeNames.value = treeComponents.value.map(item => item.id)
res.data.forEach(item => {
item.children.forEach(k => {
activeNames1.value.push(k.id)
})
})
treeComponentsCopy.value = tree2List(JSON.parse(JSON.stringify(res.data)), 0)
})
if (query.id) {
queryById({ id: query.id }).then(res => {
layout.value = JSON.parse(res.data.containerConfig)
form.pageName = res.data.pageName
form.sort = res.data.sort
form.remark = res.data.remark
form.id = res.data.id
})
}
}
// 扁平化树
const tree2List = (list: any, id: any) => {
//存储结果的数组
let arr: any = []
// 遍历 tree 数组
list.forEach((item: any) => {
item.uPid = id
item.uId = Math.random() * 1000
// 判断item是否存在children
if (!item.children) return arr.push(item)
// 函数递归对children数组进行tree2List的转换
const children = tree2List(item.children, item.uId)
// 删除item的children属性
delete item.children
// 把item和children数组添加至结果数组
//..children: 意思是把children数组展开
arr.push(item, ...children)
})
// 返回结果数组
return arr
}
// 删除拖拽
const removeItem = (id: string) => {
const index = layout.value.findIndex(item => item.i === id)
if (index > -1) {
layout.value.splice(index, 1)
}
}
const wrapper = ref<HTMLElement>()
const gridLayout = ref<InstanceType<typeof GridLayout>>()
const mouseAt = { x: -1, y: -1 }
function syncMousePosition(event: MouseEvent) {
mouseAt.x = event.clientX
mouseAt.y = event.clientY
}
const dropId = 'drop'
const dragItem = { x: -1, y: -1, w: 4, h: 2, i: '' }
const drag = throttle(row => {
// console.log("🚀 ~ drag ~ row:", row)
const parentRect = wrapper.value?.getBoundingClientRect()
if (!parentRect || !gridLayout.value) return
const mouseInGrid =
mouseAt.x > parentRect.left &&
mouseAt.x < parentRect.right &&
mouseAt.y > parentRect.top &&
mouseAt.y < parentRect.bottom
if (mouseInGrid && !layout.value.find(item => item.i === dropId)) {
layout.value.push({
x: (layout.value.length * 2) % 6,
y: layout.value.length + 6, // puts it at the bottom
w: 4,
h: 2,
i: dropId
})
}
const index = layout.value.findIndex(item => item.i === dropId)
if (index !== -1) {
const item = gridLayout.value.getItem(dropId)
if (!item) return
try {
item.wrapper.style.display = 'none'
} catch (e) {}
Object.assign(item.state, {
top: mouseAt.y - parentRect.top,
left: mouseAt.x - parentRect.left
})
const newPos = item.calcXY(mouseAt.y - parentRect.top, mouseAt.x - parentRect.left)
if (mouseInGrid) {
gridLayout.value.dragEvent('dragstart', dropId, newPos.x, newPos.y, dragItem.h, dragItem.w)
dragItem.i = String(index)
dragItem.x = layout.value[index].x
dragItem.y = layout.value[index].y
} else {
gridLayout.value.dragEvent('dragend', dropId, newPos.x, newPos.y, dragItem.h, dragItem.w)
layout.value = layout.value.filter(item => item.i !== dropId)
}
}
})
function dragEnd(row: any) {
console.log('🚀 ~ drag ~ row:', row)
const parentRect = wrapper.value?.getBoundingClientRect()
if (!parentRect || !gridLayout.value) return
const mouseInGrid =
mouseAt.x > parentRect.left &&
mouseAt.x < parentRect.right &&
mouseAt.y > parentRect.top &&
mouseAt.y < parentRect.bottom
if (mouseInGrid) {
gridLayout.value.dragEvent('dragend', dropId, dragItem.x, dragItem.y, dragItem.h, dragItem.w)
layout.value = layout.value.filter(item => item.i !== dropId)
} else {
return
}
layout.value.push({
x: dragItem.x,
y: dragItem.y,
w: dragItem.w,
h: dragItem.h,
i: dragItem.i,
name: row.name,
path: row.path,
icon: row.icon,
timeKey: row.timeKey
})
gridLayout.value.dragEvent('dragend', dragItem.i, dragItem.x, dragItem.y, dragItem.h, dragItem.w)
const item = gridLayout.value.getItem(dropId)
if (!item) return
try {
item.wrapper.style.display = ''
} catch (e) {}
}
// 保存
const onSubmit = () => {
if (layout.value.length == 0) {
return ElMessage.warning('页面设计不能为空!')
}
const maxValue = Math.max(...layout.value.map(item => item.y + item.h))
if (maxValue > 6) {
return ElMessage.warning('组件不能超出当前容器!')
}
formRef.value.validate(async (valid: boolean) => {
let url = ''
await html2canvas(document.querySelector('.GridLayout'), {
useCORS: true
}).then(canvas => {
url = canvas.toDataURL('image/png')
})
if (valid) {
if (form.id == '') {
addDashboard({ ...form, containerConfig: JSON.stringify(layout.value), thumbnail: url }).then(
(res: any) => {
ElMessage.success('新增页面成功!')
go(-1)
}
)
} else {
updateDashboard({ ...form, containerConfig: JSON.stringify(layout.value), thumbnail: url }).then(
(res: any) => {
ElMessage.success('修改页面成功!')
go(-1)
}
)
}
}
})
}
const getImg = throttle((path: string) => {
if (path != undefined) return treeComponentsCopy.value.filter(item => item.path == path)[0]?.image
})
onMounted(() => {
info()
GridHeight.value = wrapper.value?.offsetWidth / 1.77777
rowHeight.value = GridHeight.value / 6 - 11.5
document.documentElement.style.setProperty('--GridLayout-height', rowHeight.value + 10 + 'px')
document.addEventListener('dragover', syncMousePosition)
})
onBeforeUnmount(() => {
document.removeEventListener('dragover', syncMousePosition)
})
// onMounted(() => {
// // document.addEventListener('dragover', syncMousePosition)
// })
</script>
<style lang="scss" scoped>
.form-four {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.el-form-item {
display: flex;
flex: 1;
align-items: center;
.el-form-item__content {
width: 100%;
flex: 1;
.el-select,
.el-cascader,
.el-input__inner,
.el-date-editor {
width: 100%;
}
}
}
}
:deep(.el-card__body) {
padding: 20px 20px 12px;
}
.Box {
display: flex;
flex-wrap: wrap;
}
.imgBox {
// padding: 10px;
display: flex;
flex-direction: column;
box-shadow: var(--el-box-shadow-light);
--el-card-border-color: var(--el-border-color-light);
--el-card-border-radius: 4px;
--el-card-padding: 20px;
--el-card-bg-color: var(--el-fill-color-blank);
background-color: var(--el-card-bg-color);
border: 1px solid var(--el-card-border-color);
border-radius: var(--el-card-border-radius);
color: var(--el-text-color-primary);
overflow: hidden;
transition: var(--el-transition-duration) 0.3s;
.textName {
padding: 2px 5px;
background-color: var(--el-color-primary);
color: #fff;
}
user-select: none; /* 标准属性 */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
}
.vgl-layout {
background-color: #eee;
}
.remove {
position: absolute;
top: 5px;
right: 2px;
width: 16px;
color: #fff;
cursor: pointer;
}
.vgl-layout::before {
position: absolute;
width: calc(100% - 5px);
height: calc(100% - 5px);
margin: 5px;
content: '';
background-image: linear-gradient(to right, lightgrey 1px, transparent 1px),
linear-gradient(to bottom, lightgrey 1px, transparent 1px);
background-repeat: repeat;
background-size: calc(calc(100% - 5px) / 12) var(--GridLayout-height);
}
:deep(.vgl-item:not(.vgl-item--placeholder)) {
background-color: #fff;
border: 1px solid black;
}
</style>