Files
admin-govern/src/views/pqs/cockpit/setUp/components/popup.vue
2026-06-08 18:34:49 +08:00

693 lines
25 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="default-main pd10">
<div class="elButton">
<el-button type="primary" icon="el-icon-Check" @click="onSubmit">保存</el-button>
<back-component />
</div>
<div style="display: flex">
<el-tabs v-model="tableName" v-if="treeComponents.length > 0" type="border-card" @tab-change="changeTab">
<el-tab-pane v-for="item in treeComponents" :key="item.name" :label="item.name" :name="item.name">
<el-tabs v-model="tableName1" tab-position="top">
<el-tab-pane v-for="k in item?.children" :key="k.name" :label="k.name" :name="k.name">
<div :style="indicatorHeight" style="overflow-y: auto; overflow-x: hidden">
<el-row :gutter="10" class="pl5 pr5 pt5" style="width: 520px">
<el-col :span="8" v-for="component in k.children" :key="component.id" class="mb10">
<el-card
class="box-card"
shadow="hover"
@drag="drag(component)"
@dragend="dragEnd(component)"
>
<div slot="header" class="clearfix">
<span style="display: flex; align-items: center">
{{ component.name }}
</span>
</div>
<img v-if="component.image" :src="component.image" class="image xiaoshou" />
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
</el-tabs>
<el-empty v-else description="当前角色未绑定系统" style="width: 520px" />
<div style="flex: 1" ref="wrapper" class="ml10">
<el-tabs type="border-card" class="mb10">
<el-tab-pane label="基础信息">
<el-form ref="formRef" inline :rules="rules" :model="form" label-width="auto" class="form-four">
<el-form-item label="页面名称" prop="pageName">
<el-input maxlength="32" show-word-limit
style="width: 100%"
v-model.trim="form.pageName"
placeholder="请输入页面名称"
></el-input>
</el-form-item>
<el-form-item label="图标">
<IconSelector v-model.trim="form.icon" placeholder="请选择图标" />
</el-form-item>
<el-form-item label="页面排序" prop="sort">
<el-input-number
style="width: 100%"
v-model.trim="form.sort"
:min="0"
:max="10000"
:step="1"
/>
</el-form-item>
<el-form-item label="是否激活">
<el-switch
v-model="form.state"
inline-prompt
width="60px"
:disabled="form.pagePath == 'dashboard/index'"
:active-value="1"
:inactive-value="0"
active-text=" "
inactive-text=" "
/>
</el-form-item>
<el-form-item label="是否全局" v-if="hasAdmin">
<el-switch
v-model="form.scope"
inline-prompt
width="60px"
:active-value="1"
:inactive-value="0"
:disabled="form.pagePath == 'dashboard/index'"
active-text=" "
inactive-text=" "
/>
</el-form-item>
<el-form-item label="备注" class="top">
<el-input show-word-limit
maxlength="300"
style="width: 100%"
type="textarea"
:rows="1"
placeholder="请输入内容"
v-model.trim="form.remark"
></el-input>
</el-form-item>
<el-form-item></el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<div ref="PaneRef">
<el-tabs type="border-card">
<el-tab-pane label="画布" :style="height">
<GridLayout
class="GridLayout"
ref="gridLayout"
v-model:layout="layout"
style="width: 100%"
:style="{ height: GridHeight + 'px' }"
:row-height="rowHeight"
:col-num="12"
prevent-collision
:is-bounded="true"
: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>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } 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 IconSelector from '@/components/baInput/components/iconSelector.vue'
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'
import { getMenu } from '@/utils/router'
import { useAdminInfo } from '@/stores/adminInfo'
const tableName = ref('')
const tableName1 = ref('')
import { useNavTabs } from '@/stores/navTabs'
import { getSystemByRoleId } from '@/api/user-boot/roleFuction'
import { forEach } from 'jszip'
import { id } from 'element-plus/es/locale'
// defineOptions({
// name: 'cockpit/popup'
// })
const adminInfo = useAdminInfo()
const hasAdmin =
adminInfo.roleCode.some(item => item.includes('operation_manager') || item.includes('root')) ||
adminInfo.userType == 1
const { go } = useRouter()
const navTabs = useNavTabs()
const { query } = useRoute()
const router = useRouter()
const height = mainHeight(265)
const indicatorHeight = mainHeight(180)
const rowHeight = ref(0)
const pageList: any = ref([])
const GridHeight = ref(0)
const position = ref<CollapseIconPositionType>('left')
const form: any = reactive({
pageName: '',
thumbnail: '',
containerConfig: [],
sort: '100',
id: '',
state: 1,
scope: 0,
icon: '',
pagePath: '',
remark: '',
routeName: '/src/views/pqs/cockpit/homePage/index.vue'
})
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 = async () => {
pageList.value = router
.getRoutes()
.filter(item => item?.meta?.component == '/src/views/pqs/cockpit/homePage/index.vue')
.map(item => {
return { name: item?.meta?.title, path: item?.meta?.path }
})
activeNames.value = []
activeNames1.value = []
let systemList: any[] = []
await adminInfo.roleList.forEach((item: any) => {
getSystemByRoleId({ id: item }).then(res => {
systemList = [...new Set([...systemList, ...res.data.systemIds])]
})
})
await componentTree().then(res => {
let list = res.data.filter(item => systemList.includes(item.id))
treeComponents.value = list
tableName.value = tableName.value == '' ? treeComponents.value[0].name : tableName.value
tableName1.value = tableName1.value == '' ? treeComponents.value[0].children[0].name : tableName1.value
activeNames.value = treeComponents.value.map(item => item.id)
list.forEach(item => {
item.children.forEach(k => {
activeNames1.value.push(k.id)
})
})
treeComponentsCopy.value = tree2List(JSON.parse(JSON.stringify(list)), 0)
})
if (query.id) {
queryById({ id: query.id }).then(res => {
layout.value = JSON.parse(res.data.containerConfig)
form.pageName = res.data.pageName
form.pagePath = query.path || res.data.pagePath
form.sort = res.data.sort
form.remark = res.data.remark
form.id = res.data.id
form.state = res.data.state
form.scope = res.data.userId == 0 ? 1 : 0
form.icon = res.data.icon
})
} else {
form.pagePath = query.path
}
}
// 扁平化树
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 changeTab = (e: any) => {
console.log('🚀 ~ changeTab ~ e:', e)
tableName1.value = treeComponents.filter(item => item.name == e)[0].children[0].name
}
// 删除拖拽
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 PaneRef = 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 - 300,
left: mouseAt.x - parentRect.left - 180
})
const newPos = item.calcXY(mouseAt.y - parentRect.top - 300, mouseAt.x - parentRect.left - 180)
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) {
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: Math.random(), //dragItem.i,
name: row.name,
path: row.path,
icon: row.icon,
timeKeys: row.timeKeys
})
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 = () => {
layout.value = layout.value.filter(item => {
return item.y <= 6 && item.x <= 12
})
if (layout.value.length == 0) {
return ElMessage.warning('页面设计不能为空!')
}
let repeat = findDuplicateNames(layout.value) || []
console.log('🚀 ~ onSubmit ~ layout.value:', layout.value)
if (repeat.length > 0) {
return ElMessage.warning(repeat.join('、') + ' 组件重复,请删除重复组件!')
}
// 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')
// })
form.pagePath = form.pagePath || ''
if (valid) {
if (form.id == '') {
await addDashboard({
...form,
containerConfig: JSON.stringify(layout.value),
thumbnail: url,
userId: form.scope == 1 ? '0' : adminInfo.id
}).then(async (res: any) => {
ElMessage.success('新增页面成功!')
// go(-1)
await getMenu()
})
} else {
await updateDashboard({
...form,
containerConfig: JSON.stringify(layout.value),
thumbnail: url,
userId: form.scope == 1 ? '0' : adminInfo.id
}).then(async (res: any) => {
ElMessage.success('修改页面成功!')
// go(-1)
await getMenu()
})
}
await setTimeout(() => {
router.push({
name: form.state == 1 ? form.pagePath : 'dashboard/index'
})
navTabs.refresh()
}, 500)
}
})
}
// 查找重复的name
// const findDuplicateNames = (arr: any) => {
// // 用于记录每个name出现的次数
// const nameCount = {}
// // 用于存储重复的name
// const duplicates = []
// // 遍历数组统计每个name的出现次数
// arr.forEach(item => {
// const name = item.name
// if (nameCount[name]) {
// nameCount[name]++
// } else {
// nameCount[name] = 1
// }
// })
// // 筛选出出现次数大于1的name
// for (const name in nameCount) {
// if (nameCount[name] > 1) {
// duplicates.push(name)
// }
// }
// return duplicates
// }
// 查找重复的path并返回对应的重复name
const findDuplicateNames = (arr: any) => {
// 用于记录每个path出现的次数
const pathCount: any = {}
// 用于存储path对应的name避免重复存储
const pathToNameMap = new Map()
// 用于存储重复path对应的name
const duplicates = []
// 遍历数组统计每个path的出现次数并记录path和name的映射
arr.forEach(item => {
const path = item.path
const name = item.name
// 记录path和name的对应关系只存第一次的避免覆盖
if (!pathToNameMap.has(path)) {
pathToNameMap.set(path, name)
}
// 统计path出现次数
if (pathCount[path]) {
pathCount[path]++
} else {
pathCount[path] = 1
}
})
// 筛选出出现次数大于1的path并获取对应的name
for (const path in pathCount) {
if (pathCount[path] > 1) {
const name = pathToNameMap.get(path)
// 避免重复添加相同的name如果多个重复path对应同一个name
if (!duplicates.includes(name)) {
duplicates.push(name)
}
}
}
return duplicates
}
const getImg = throttle((path: string) => {
if (path != undefined) return treeComponentsCopy.value.filter(item => item.path == path)[0]?.image
})
onMounted(() => {
info()
nextTick(() => {
GridHeight.value = PaneRef.value?.offsetHeight - 60 //wrapper.value?.offsetWidth / 1.77777
setTimeout(() => {
console.log('🚀 ~ wrapper.value?.offsetWidth:', PaneRef.value?.offsetWidth)
}, 500)
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;
width: 32%;
.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;
}
:deep(.el-input-group) {
width: 90%;
}
.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;
}
:deep(.el-form--inline .el-form-item) {
margin-right: 0px;
}
:deep(.el-card__header) {
padding: 13px 20px;
height: 44px;
}
:deep(.el-card__body) {
padding: 10px;
}
.xiaoshou {
cursor: pointer;
}
.image {
display: block;
width: 100%;
height: 100px;
}
.box-card {
width: 100%;
// border: 1px solid #000;
box-shadow: var(--el-box-shadow-light);
}
.elButton {
position: absolute;
right: 10px;
top: 13px;
z-index: 1;
}
:deep(.vgl-item__resizer) {
width: 20px;
height: 20px;
background: linear-gradient(45deg, var(--el-color-primary));
clip-path: polygon(100% 0, 100% 100%, 0 100%);
/* 可选:添加阴影增强视觉效果 */
box-shadow: 2px 2px 8px var(--el-color-primary);
}
</style>