25 Commits

Author SHA1 Message Date
stt
2698ca4f5c 联调实时数据表格 2025-12-09 13:22:04 +08:00
guanj
da26b1d237 修改组态 2025-12-09 08:41:11 +08:00
stt
d586f19bd9 弹框修改 2025-12-05 14:10:29 +08:00
stt
be63b12db7 样式修改 2025-12-05 11:27:44 +08:00
stt
346346c3f9 删除多余代码 2025-12-05 11:26:16 +08:00
stt
178054426d 样式修改 2025-12-05 11:25:26 +08:00
guanj
761ad3c2f8 修改传入数据 2025-12-05 11:22:36 +08:00
stt
6d08a7673e 重新提交 2025-12-05 11:00:15 +08:00
guanj
ac0774cc74 调整实时数据 2025-12-05 10:55:28 +08:00
stt
c013158f7c 弹框 2025-12-05 10:45:02 +08:00
stt
f706c49e93 连接mqtt 2025-12-04 16:28:37 +08:00
stt
6a92786f60 Merge branch 'master' of http://192.168.1.22:3000/Web/svgeditor2.0 2025-12-01 14:14:23 +08:00
stt
e8ce98f80c 加入ypt判断 2025-12-01 14:14:11 +08:00
guanj
9d7d7c0cbd 修改导入json 2025-11-14 14:08:01 +08:00
guanj
9bd9403280 修改测试用例 2025-11-12 09:52:48 +08:00
guanj
3d987b4761 修改预览展示 2025-11-05 15:31:56 +08:00
guanj
a7a88e6706 修改点离线样式 2025-11-03 10:36:11 +08:00
guanj
99ad7a3021 修改接线图展示页面 2025-10-22 11:42:14 +08:00
stt
222ec77df1 画布拖拽的方法提出来 2025-10-22 09:49:14 +08:00
stt
77636e502f 云平台预览 2025-10-22 09:09:46 +08:00
stt
4df52a2c87 Merge branch 'master' of http://192.168.1.22:3000/Web/svgeditor2.0 2025-10-22 09:08:59 +08:00
stt
26c2190ded 云平台预览 2025-10-22 09:08:29 +08:00
guanj
e10b451e68 修改截图样式不全问题 2025-10-22 08:50:42 +08:00
guanj
78a4b1685c 添加排序 2025-10-14 20:38:59 +08:00
guanj
77b35d3395 修改线流动逻辑 2025-10-14 19:44:07 +08:00
30 changed files with 2713 additions and 107 deletions

View File

@@ -78,6 +78,13 @@ export function targetList(params: any) {
params: params
})
}
// 云平台
export function eleEpdChooseTree_ypt() {
return http.request({
url: '/cs-system-boot/csDictData/eleEpdChooseTree',
method: 'GET'
})
}
// 无锡指标列表
export function eleEpdChooseTree_wx() {
return http.request({

View File

@@ -1,6 +1,13 @@
<template>
<div class="add-element">
<el-dialog v-model="open" title="新增图元" width="500px" destroy-on-close @close="closeDialog">
<el-dialog
:close-on-click-modal="false"
v-model="open"
title="新增图元"
width="500px"
destroy-on-close
@close="closeDialog"
>
<el-form :model="element" ref="ruleFormRef" :rules="rules" label-width="120px">
<el-form-item label="图元分类:" prop="elementSonType">
<el-select v-model="element.elementSonType" placeholder="请选择图元分类" style="width: 100%">
@@ -72,7 +79,14 @@
/>
</el-form-item> -->
<el-form-item label="图元名称:" prop="elementName">
<el-input v-model="element.elementName" placeholder="请选择组件名称" style="width: 100%" />
<el-input
v-model.trim="element.elementName"
maxlength="12"
show-word-limit
clearable
placeholder="请选择组件名称"
style="width: 100%"
/>
</el-form-item>
<!-- <el-form-item label="组件标识:" prop="elementMark">
<el-input v-model="element.elementMark" placeholder="请选择组件标识" style="width: 100%" />
@@ -93,7 +107,7 @@
>
<el-button type="primary">上传</el-button>
</el-upload>
<el-dialog v-model="dialogVisible">
<el-dialog :close-on-click-modal="false" v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</el-form-item>
@@ -148,6 +162,10 @@ const options = [
{
value: '自定义',
label: '自定义'
},
{
value: '特殊图元',
label: '特殊图元'
}
]

View File

@@ -18,7 +18,7 @@
import { VAceEditor } from 'vue3-ace-editor'
import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '../../store/types'
import { computed } from 'vue'
import { genExportJson } from '../../composables'
import { genExportJson } from '@/components/mt-edit/composables/index'
type ExportProps = {
doneJson: IDoneJson[]
canvasCfg: IGlobalStoreCanvasCfg

View File

@@ -25,10 +25,7 @@ const onImport = () => {
return new Promise((resolve, reject) => {
try {
const json: IExportJson = JSON.parse(import_json.value)
console.log('🚀 ~ onImport ~ json:', json)
const { canvasCfg, gridCfg, importDoneJson } = useExportJsonToDoneJson(json)
globalStore.canvasCfg = canvasCfg
globalStore.gridCfg = gridCfg
globalStore.setGlobalStoreDoneJson(importDoneJson)

View File

@@ -55,12 +55,12 @@
</el-button>
<el-divider direction="vertical"></el-divider>
<el-button-group>
<el-button text circle size="small" @click="onImportClick">
<el-button text circle size="small" @click="onImportClick" :disabled="useData.keyName == ''">
<el-icon title="导入数据模型" :size="20">
<svg-analysis name="import-json"></svg-analysis>
</el-icon>
</el-button>
<el-button text circle size="small" @click="onExportClick">
<el-button text circle size="small" @click="onExportClick" :disabled="useData.keyName == ''">
<el-icon title="导出数据模型" :size="20">
<svg-analysis name="export-json"></svg-analysis>
</el-icon>
@@ -158,11 +158,11 @@
</el-tag>
</div>
<div class="flex items-center mr-20px">
<el-button text circle size="small" @click="emits('onReturnClick')">
<!-- <el-button text circle size="small" @click="emits('onReturnClick')">
<el-icon title="返回" :size="20">
<svg-analysis name="return"></svg-analysis>
</el-icon>
</el-button>
</el-button> -->
<!-- <el-divider direction="vertical"></el-divider> -->
<!-- <el-button text circle size="small" @click="emits('onSaveClick')">
<el-icon title="保存" :size="20">
@@ -229,7 +229,9 @@ import { useDark, useToggle, useFullscreen } from '@vueuse/core'
import { ElIcon, ElDivider, ElPopover, ElButton, ElButtonGroup, ElImage, ElText, ElTag } from 'element-plus'
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
import type { IRealTimeData } from '@/components/mt-edit/store/types'
import { useDataStore } from '@/stores/menuList'
import { ref } from 'vue'
const useData = useDataStore()
type HeaderPanelProps = {
leftAside: boolean
rightAside: boolean

View File

@@ -12,7 +12,7 @@
ref="multipleTable"
@row-click="onRowClick"
empty-text="暂无数据"
row-key="id"
row-key="name"
>
<el-table-column prop="name" label="名称" />
<el-table-column label="操作" align="center" width="60" #default="scope">
@@ -22,7 +22,7 @@
</el-icon>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-icon @click.stop="del(scope.$index)" style="margin-left: 5px; cursor: pointer">
<el-icon @click.stop="del(scope.$index, scope.row)" style="margin-left: 5px; cursor: pointer">
<Delete />
</el-icon>
</el-tooltip>
@@ -30,16 +30,30 @@
<el-table-column label="操作" width="40">
<template #default>
<el-tooltip content="拖拽" placement="top">
<div class="drag-handle"></div>
<div class="drag-handle" style="cursor: pointer"></div>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 新增/修改 -->
<el-dialog draggable v-model="dialogFormVisible" :title="dialog_title" width="500px" destroy-on-close>
<el-dialog
:close-on-click-modal="false"
draggable
v-model="dialogFormVisible"
:title="dialog_title"
width="500px"
destroy-on-close
>
<el-form :model="form" ref="formRef" :rules="rules">
<el-form-item label="图纸名称" :label-width="formLabelWidth" prop="name">
<el-input v-model="form.name" autocomplete="off" placeholder="请输入图纸名称" />
<el-input
v-model.trim="form.name"
maxlength="12"
show-word-limit
clearable
autocomplete="off"
placeholder="请输入图纸名称"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -80,7 +94,6 @@ const useData = useDataStore()
const dialogFormVisible = ref(false)
const dialog_title = ref('新增图纸')
const formLabelWidth = '100px'
const globalIndex = ref(-1)
const multipleTable: any = ref(null)
const { pid } = useData // 解构出 myArray 状态
@@ -122,31 +135,45 @@ const sortableInstance = ref<any>(null)
// 修改 initSortable 方法中的实例挂载与销毁逻辑
const initSortable = () => {
nextTick(() => {
const tbody = multipleTable.value.$el.querySelector('.el-table__body-wrapper tbody')
if (!tbody) {
console.error('未找到 tbody 元素')
return
}
// 销毁旧实例
if (sortableInstance.value) {
sortableInstance.value.destroy()
}
// 创建新实例并保存到 sortableInstance
sortableInstance.value = new Sortable(tbody, {
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: ({ newIndex, oldIndex }) => {
// 确保 newIndex 和 oldIndex 都存在且不相等
if (newIndex === undefined || oldIndex === undefined || newIndex === oldIndex) return
const targetItem = dataTrees.value.splice(oldIndex, 1)[0]
dataTrees.value.splice(newIndex, 0, targetItem)
Sortable.create(tbody, {
animation: 100,
onEnd: ({ oldIndex, newIndex }) => {
const currRow = dataTrees.value.splice(oldIndex, 1)[0]
dataTrees.value.splice(newIndex, 0, currRow)
}
})
})
// nextTick(() => {
// const tbody = multipleTable.value.$el.querySelector('.el-table__body-wrapper tbody')
// if (!tbody) {
// console.error('未找到 tbody 元素')
// return
// }
// // 销毁旧实例
// if (sortableInstance.value) {
// sortableInstance.value.destroy()
// }
// // 创建新实例并保存到 sortableInstance
// sortableInstance.value = new Sortable(tbody, {
// animation: 150,
// ghostClass: 'sortable-ghost',
// onEnd: ({ newIndex, oldIndex }) => {
// // 确保 newIndex 和 oldIndex 都存在且不相等
// if (newIndex === undefined || oldIndex === undefined || newIndex === oldIndex) return
// // const targetItem = dataTrees.value.splice(oldIndex, 1)[0]
// // dataTrees.value.splice(newIndex, 0, targetItem)
// // 深拷贝避免引用问题(如果是复杂对象,建议使用结构化克隆或深拷贝工具)
// const [targetItem] = dataTrees.value.splice(oldIndex, 1)
// // 安全插入(超出范围时自动插入到数组末尾)
// const insertIndex = Math.min(newIndex, dataTrees.value.length)
// dataTrees.value.splice(insertIndex, 0, targetItem)
// }
// })
// })
}
//form表单校验规则
@@ -171,13 +198,14 @@ watch(
)
const onAddClick = () => {
dialog_title.value = '新增图纸'
Object.assign(form, { name: '' })
//打开弹窗
dialogFormVisible.value = true
}
// 删除功能,传索引行数
function del(index: number) {
function del(index: number, row: any) {
ElMessageBox.confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -186,6 +214,7 @@ function del(index: number) {
.then(() => {
// splice方法传两个参数第几行开始删除多少条如果未规定此参数则删除从 index 开始到原数组结尾的所有元素)
dataTrees.value.splice(index, 1)
useData.placeKid(row.kId)
ElMessage({
type: 'success',
message: '删除成功'
@@ -213,17 +242,22 @@ const onRowClick = async (row: any) => {
}
const saveDialog = async (form: any) => {
console.log(dataTrees.value)
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
if (dataTrees.value.some(item => item.name == form.name)) {
ElMessage.error('图纸名称不能重复!')
return
}
// 提交请求
if (globalIndex.value >= 0) {
if (dialog_title.value == '编辑图纸') {
//表示编辑
// dataTrees[globalIndex.value] = form;
useData.modify(form.kId, form.name)
//还原回去
globalIndex.value = -1
} else {
//新增
useData.append(form.name)
@@ -236,9 +270,9 @@ const saveDialog = async (form: any) => {
function update(index: number, row: any) {
// const newObj = Object.assign({}, row);
// form = reactive(newObj);
dialog_title.value = '编辑图纸'
Object.assign(form, row)
//把当前编辑的行号赋值给全局保存的行号
globalIndex.value = index
dialogFormVisible.value = true
}
</script>

View File

@@ -64,7 +64,13 @@
<div class="h-[calc(10%-1px)] flex justify-center items-center ct-border" style="padding-top: 10px">
<el-button class="w-80/100" @click="onManageClick">管理</el-button>
</div>
<el-dialog v-model="manage_dialog_visiable" title="图库管理" width="50%" destroy-on-close>
<el-dialog
:close-on-click-modal="false"
v-model="manage_dialog_visiable"
title="图库管理"
width="50%"
destroy-on-close
>
<div class="flex">
<div>
<div>

View File

@@ -1420,6 +1420,9 @@ watch(
time.value = setTimeout(() => {
console.log('🚀 ~ globalStore:', globalStore)
if (useData.keyName == '') {
ElMessage.warning('请选择图纸!')
}
const { exportJson } = genExportJson(globalStore.canvasCfg, globalStore.gridCfg, globalStore.done_json)
// const data_model: any = {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-button type="primary" plain round @click="dialogVisible = true">点击编辑</el-button>
<el-dialog v-model="dialogVisible" title="配置编辑" width="60%">
<el-dialog :close-on-click-modal="false" v-model="dialogVisible" title="配置编辑" width="60%">
<v-ace-editor
v-model:value="content"
lang="json"

View File

@@ -1,12 +1,7 @@
<template>
<div v-if="useData.graphicDisplay == 'zl'">
<el-form label-width="60px" label-position="left">
<el-form-item
label="监测点"
size="small"
class="mt-10px"
v-if="item_title == '绑定监测点' || item_title == '绑定指标'"
>
<el-form-item label="监测点" size="small" class="mt-10px">
<div>
<el-cascader
:key="cascaderKey"
@@ -47,13 +42,13 @@
</el-form>
</div>
<!-- 无锡监测点 指标 -->
<div v-if="useData.graphicDisplay == 'wx'">
<div v-if="useData.graphicDisplay == 'wx' || useData.graphicDisplay == 'ypt'">
<el-form label-width="60px" label-position="left">
<el-form-item label="监测点" size="small" class="mt-10px">
<div>
<el-cascader
:key="cascaderKey"
:options="treeData_wx"
:options="useData.graphicDisplay == 'ypt' ? treeData : treeData_wx"
:props="{ value: 'id', label: 'name', children: 'children' }"
clearable
placeholder="请选择"
@@ -110,7 +105,7 @@ import { ElForm, ElFormItem, ElTreeSelect } from 'element-plus'
import { computed, ref, watch, onMounted, reactive, nextTick, watchEffect } from 'vue'
import type { IDoneJson } from '@/components/mt-edit/store/types'
import { globalStore } from '@/components/mt-edit/store/global'
import { lineTree, targetList, eleEpdChooseTree_wx } from '@/api/index'
import { lineTree, targetList, eleEpdChooseTree_wx, eleEpdChooseTree_ypt } from '@/api/index'
import { useDataStore } from '@/stores/menuList'
import { lineTree_wx } from '@/api/index_wx'
import { templateRef } from '@vueuse/core'
@@ -195,6 +190,11 @@ watch(
if (item_title.value == '绑定指标' && deptIds.value) {
indexList()
}
// if (useData.graphicDisplay == 'ypt') {
// if (deptIds.value) {
// indexList()
// }
// }
})
}
})
@@ -267,6 +267,10 @@ const fetchData = async () => {
try {
const response = await lineTree({})
treeData.value = buildLevel3Tree(response.data) // 转换数据格式并赋值给 transformedData
if (useData.graphicDisplay == 'ypt') {
const res = await eleEpdChooseTree_ypt()
treeIndexs.value = res.data // 转换数据格式并赋值给 transformedData
}
} catch (error) {
console.error('Error fetching data:', error)
}
@@ -289,7 +293,13 @@ const fetchData_wx = async () => {
// 指标数据
const indexList = async () => {
try {
const lineId = deptIds.value[deptIds.value.length - 1]
let lineId = ''
if (typeof deptIds.value === 'string') {
lineId = deptIds.value
} else {
lineId = deptIds.value[deptIds.value.length - 1]
}
{
const response = await targetList({ lineId: lineId })
if (response.data) {
@@ -304,6 +314,7 @@ const indexList = async () => {
const handleDeptChange = (deptId: []) => {
// labelString.value = fileRef.value.getCheckedNodes().pathLabels.join(" / ");
item_uid.value = []
indexString.value = ''
nextTick(() => {
//fileRef.value.getCheckedNodes()[0]?.label 最后一层的值
let name = []
@@ -332,6 +343,12 @@ const handleDeptChange = (deptId: []) => {
indexList()
}
}
// if (useData.graphicDisplay == 'ypt') {
// if (deptId) {
// indexList()
// }
// }
})
}
// 给每一个元件绑定下拉框数据
@@ -342,7 +359,6 @@ const handleSelectUID = (uid: []) => {
let nodes = []
if (indexRef.value) {
nodes = indexRef.value.getCheckedNodes()
console.log('🚀 ~ handleSelectUID ~ indexRef.value.getCheckedNodes():', indexRef.value.getCheckedNodes())
name = nodes[0]?.pathLabels || []
}
if (selectItemSettingProps.itemJson) {
@@ -354,7 +370,6 @@ const handleSelectUID = (uid: []) => {
selectItemSettingProps.itemJson.UIDName = name.join('/')
}
if (is2DArray(uid)) {
selectItemSettingProps.itemJson.UIDNames = name.join(' / ')
} else {

View File

@@ -222,7 +222,12 @@
</div>
</el-form>
</el-drawer>
<el-dialog v-model="dialog_visiable" :title="dialog_title" :before-close="onDialogClose">
<el-dialog
:close-on-click-modal="false"
v-model="dialog_visiable"
:title="dialog_title"
:before-close="onDialogClose"
>
<v-ace-editor
v-model:value="dialog_code"
lang="javascript"

View File

@@ -68,7 +68,12 @@
@set-intention="val => renderCoreEmits('setIntention', val)"
@line-mouse-up="onLineMouseUp"
v-on="renderCoreProps.preivewMode ? eventToVOn(item, externalMethod) : {}"
@click="() => renderCoreProps.onElementClick && item.lineId && renderCoreProps.onElementClick(item.lineId)"
@click="
() =>
renderCoreProps.onElementClick &&
item.lineId &&
renderCoreProps.onElementClick(item.lineId, item.lineName)
"
></render-item>
</mt-dzr>
</template>
@@ -130,7 +135,7 @@ type RenderCoreProps = {
preivewMode?: boolean
lineAppendEnable?: boolean
showPopover?: boolean
onElementClick?: (elementId: string) => void
onElementClick?: (elementId: string, lineName?: string) => void
}
const renderCoreProps = withDefaults(defineProps<RenderCoreProps>(), {
doneJson: () => [],

View File

@@ -10,6 +10,7 @@ export const useGenThumbnail = async (canvas_id: string = 'mtCanvasArea') => {
// //记录要移除的svg元素
const shouldRemoveSvgNodes = []
// 获取到所有的SVG 得到一个数组 目前只有自定义连线需要特殊处理 别的元素直接使用html2canvas就可以
const svgElements: NodeListOf<HTMLElement> = document.body.querySelectorAll(`#${canvas_id} .mt-line-render`)
// 遍历这个数组
for (const item of svgElements) {
@@ -40,13 +41,14 @@ export const useGenThumbnail = async (canvas_id: string = 'mtCanvasArea') => {
const width = el.offsetWidth
const height = el.offsetHeight
const canvas = await html2canvas(el, {
useCORS: true,
scale: 2,
width,
height,
// width,
// height,
allowTaint: true,
windowHeight: height,
// windowHeight: height,
logging: false,
ignoreElements: element => {
if (element.classList.contains('mt-line-render')) {

View File

@@ -72,13 +72,36 @@
<footer-panel></footer-panel>
</el-footer>
</el-container>
<el-dialog v-model="import_visible" title="数据导入" @close="mainPanelRef?.beginListenerKeyDown()">
<!-- 上传json -->
<el-upload
:show-file-list="false"
accept=".json"
ref="uploadRef"
:auto-upload="false"
:limit="1"
:on-exceed="handleExceed"
:on-change="handleOnchange"
>
<el-button type="text" v-show="false">默认上传按钮(隐藏)</el-button>
</el-upload>
<el-dialog
:close-on-click-modal="false"
v-model="import_visible"
title="数据导入"
@close="mainPanelRef?.beginListenerKeyDown()"
>
<import-json ref="importJsonRef"></import-json>
<template #footer>
<el-button type="primary" @click="onImportYes">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="export_visible" title="数据导出" @close="mainPanelRef?.beginListenerKeyDown()">
<el-dialog
:close-on-click-modal="false"
v-model="export_visible"
title="数据导出"
@close="mainPanelRef?.beginListenerKeyDown()"
>
<export-json
:done-json="objectDeepClone(globalStore.done_json)"
:canvas-cfg="globalStore.canvasCfg"
@@ -114,6 +137,8 @@ import { objectDeepClone } from './utils'
import { genExportJson, useExportJsonToDoneJson } from './composables'
import type { IExportJson } from './components/types'
import { useDataStore } from '@/stores/menuList'
import type { UploadInstance, UploadProps, UploadRawFile, UploadFile } from 'element-plus'
import { genFileId } from 'element-plus'
type MtEditProps = {
useThumbnail?: boolean
}
@@ -150,6 +175,7 @@ const header_align_enabled = computed(() => {
)
return selected_items.length > 1
})
const uploadRef = ref()
const import_visible = ref(false)
const export_visible = ref(false)
const done_json_tree_visiable = ref(false)
@@ -159,12 +185,65 @@ const onDeleteClick = () => {
cacheStore.addHistory(globalStore.done_json)
}
const onImportClick = () => {
import_visible.value = true
mainPanelRef.value?.stopListenerKeyDown()
uploadRef.value?.$el.querySelector('input[type="file"]')?.click()
// import_visible.value = true
// mainPanelRef.value?.stopListenerKeyDown()
}
const handleExceed: UploadProps['onExceed'] = files => {
uploadRef.value!.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
uploadRef.value!.handleStart(file)
}
// 上传json文件
const handleOnchange = (uploadFile: UploadFile) => {
let file: any = uploadFile.raw // 获取文件信息
const fileName = file.name
const isJsonSuffix = fileName.endsWith('.json')
// 方式2通过文件 MIME 类型校验(辅助验证,部分浏览器可能不准确)
const isJsonType = file.type === 'application/json' || file.type === ''
if (!isJsonSuffix || !isJsonType) {
ElMessage.error('请上传后缀为 .json 的合法 JSON 文件!')
uploadRef.value.clearFiles()
return // 校验失败,终止后续操作
}
const fileReader = new FileReader()
fileReader.readAsText(file!) // 开始读取文件的内容为二进制
fileReader.onload = ev => {
// 读取完成,对数据进行自己的操作
const data = ev.target?.result //获取内容
console.log('🚀 ~ handleOnchange ~ data:', data)
onImportYes(data)
// console.log(JSON.parse(data as string))
}
}
const onExportClick = () => {
export_visible.value = true
mainPanelRef.value?.stopListenerKeyDown()
// export_visible.value = true
// mainPanelRef.value?.stopListenerKeyDown()
// :done-json="objectDeepClone(globalStore.done_json)"
// :canvas-cfg="globalStore.canvasCfg"
// :grid-cfg="globalStore.gridCfg"
const { exportJson } = genExportJson(
globalStore.canvasCfg,
globalStore.gridCfg,
objectDeepClone(globalStore.done_json)
)
let data = JSON.stringify(exportJson, null, 2)
let blob = new Blob([data], { type: 'text/plain' })
let url = URL.createObjectURL(blob)
let a = document.createElement('a')
a.href = url
a.download = useData.keyName + '.json'
a.click()
ElMessage.success('JSON 导出成功')
}
const onTreeUpdateSelectedItemsId = (id: string) => {
globalStore.setSingleSelect(id)
@@ -197,15 +276,35 @@ const onRedoClick = () => {
const onUndoClick = () => {
mainPanelRef.value?.onUndo()
}
const onImportYes = async () => {
const res = await importJsonRef.value?.onImport()
const onImportYes = async (row: any) => {
const res = await onImport(row)
// const res = await importJsonRef.value?.onImport()
if (res) {
import_visible.value = false
uploadRef.value.clearFiles()
cacheStore.addHistory(globalStore.done_json)
ElMessage.success('JSON 导入成功')
} else {
uploadRef.value.clearFiles()
ElMessage.error('导入失败,请检查数据格式')
}
}
const onImport = (res: any) => {
return new Promise((resolve, reject) => {
try {
const json: IExportJson = JSON.parse(res)
const { canvasCfg, gridCfg, importDoneJson } = useExportJsonToDoneJson(json)
globalStore.canvasCfg = canvasCfg
globalStore.gridCfg = gridCfg
globalStore.setGlobalStoreDoneJson(importDoneJson)
resolve(true)
} catch (error) {
resolve(false)
}
})
}
const onPreviewClick = () => {
// 获取导出json
const { exportJson } = genExportJson(globalStore.canvasCfg, globalStore.gridCfg, globalStore.done_json)
@@ -219,9 +318,18 @@ const onSaveClick = () => {
}
const useData = useDataStore()
const onSaveAll = () => {
const onSaveAll = async () => {
let data: any = await useData.dataTree.map((item, ind) => {
let pathList = JSON.parse(item.path)
pathList.canvasCfg.lineList = pathList.json.filter(k => k.lineId != '' && k.lineId != null).map(k => k.lineId)
item.path = JSON.stringify(pathList)
item.sort = ind
return item
})
console.log('🚀 ~ onSaveAll ~ data:', data)
let form = new FormData()
let blob = new Blob([JSON.stringify(useData.dataTree)], {
let blob = new Blob([JSON.stringify(data)], {
type: 'application/json'
})
form.append('multipartFile', blob)
@@ -251,6 +359,7 @@ defineExpose({
.mt-edit-aside {
transition: width 0.3s;
}
.mt-edit-aside-left {
padding-left: 5px;
}

View File

@@ -817,6 +817,7 @@ export const eventToVOn = (item: IDoneJson, externalMethod: (kid?: string) => vo
code_str += event.jump_to
}
}
if (!Object.prototype.hasOwnProperty.call(event_obj, event.type)) {
event_obj[event.type] = code_str
} else {

View File

@@ -0,0 +1,262 @@
<template>
<div class="container">
<!-- 使用 v-for 遍历四个角落 -->
<div v-for="corner in corners" v-show="corner.show" :key="corner.id" :class="['corner', corner.className]">
<div class="content">
<div class="title">{{ corner.title }}</div>
<el-descriptions :column="1" size="small" label-width="70px" border>
<el-descriptions-item label="指标数据">
<div style="height: 200px; overflow-y: auto">
<div
v-for="item in props.steadyState?.filter(k => k.lineId === corner.elementId)"
:key="item.id"
>
{{ item.statisticalName.replace(/\//g, '_') }}:
{{ item.value === 3.1415926 ? '/' : item.value + item.unit }}
</div>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<span class="close-btn" @click="closeCorner(corner.id)">
<Close />
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
// 定义 emits
const emit = defineEmits(['lineListChange'])
// 定义接收的 props
const props = defineProps<{
steadyState?: any[]
}>()
// 定义四个角落的数据
const corners = ref([
{
id: 'topLeft',
title: '左上',
className: 'top-left',
show: false,
data: [] as { label: string; value: string }[],
elementId: '' // 记录该角落对应的元素ID
},
{
id: 'topRight',
title: '右上',
className: 'top-right',
show: false,
data: [] as { label: string; value: string }[],
elementId: ''
}
])
const displayOrder = ref<number[]>([])
// 截取名称的最后一部分作为标题
const extractTitleFromLineName = (lineName: string): string => {
if (!lineName) return '未知监测点'
// 按照 "/" 分割字符串,取最后一部分
const parts = lineName.split('/')
return parts.length > 0 ? parts[parts.length - 1].trim() : lineName
}
// 更新指定角落数据的函数
const updateCornerData = (cornerIndex: number, elementId: string, lineName: string) => {
// 更新标题为传入的 lineName 的最后一部分
corners.value[cornerIndex].title = extractTitleFromLineName(lineName)
// 格式化数据(只保留稳态指标一项)
corners.value[cornerIndex].data = [
{
label: '稳态指标',
value: '正在加载...'
}
]
// 记录该角落对应的元素ID
corners.value[cornerIndex].elementId = elementId
corners.value[cornerIndex].show = true
}
// 显示下一个角落的函数
const showNextCorner = (elementId: string, lineName: string) => {
// 检查该元素ID是否已经显示过
const existingCornerIndex = corners.value.findIndex(corner => corner.elementId === elementId && corner.show)
if (existingCornerIndex !== -1) {
// 如果该元素已经显示过,更新标题
corners.value[existingCornerIndex].title = extractTitleFromLineName(lineName)
return
}
// 查找一个未显示的角落
const availableCornerIndex = corners.value.findIndex(corner => !corner.show)
if (availableCornerIndex !== -1) {
// 有空闲角落,显示在该角落
updateCornerData(availableCornerIndex, elementId, lineName)
// 记录显示顺序
displayOrder.value.push(availableCornerIndex)
} else {
// 没有空闲角落,按顺序替换角落
// 获取需要替换的角落索引(循环替换)
const replaceIndex = displayOrder.value.shift() || 0
updateCornerData(replaceIndex, elementId, lineName)
// 将替换的索引重新加入队列末尾
displayOrder.value.push(replaceIndex)
}
// updateLineList()
}
const timer = ref<any>(null)
// 更新 lineList根据 corners 的 show 状态来维护
const updateLineList = () => {
const newLineList = corners.value.filter(corner => corner.show && corner.elementId).map(corner => corner.elementId)
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
if (newLineList.length > 0) {
emit('lineListChange', newLineList)
timer.value = setInterval(
() => {
emit('lineListChange', newLineList)
},
3 * 60 * 1000
)
}
}
// 关闭指定角落的函数
const closeCorner = (id: string) => {
const cornerIndex = corners.value.findIndex(c => c.id === id)
if (cornerIndex !== -1) {
corners.value[cornerIndex].show = false
corners.value[cornerIndex].elementId = '' // 清空元素ID记录
// 从显示顺序中移除该角落索引
const orderIndex = displayOrder.value.indexOf(cornerIndex)
if (orderIndex !== -1) {
displayOrder.value.splice(orderIndex, 1)
}
}
}
// 关闭所有角落的函数
const closeAllCorners = () => {
corners.value.forEach(corner => {
corner.show = false
corner.elementId = ''
})
displayOrder.value = []
updateLineList()
}
// 监听 corners 的变化,特别是 show 状态的变化
watch(
() => corners.value.map(corner => ({ id: corner.id, show: corner.show, elementId: corner.elementId })),
(newCorners, oldCorners) => {
// 检查是否有 show 状态的变化
const showStateChanged = newCorners.some((corner, index) => corner.show !== oldCorners[index]?.show)
if (showStateChanged) {
updateLineList()
}
},
{ deep: true }
)
// 暴露方法给父组件使用
defineExpose({
showNextCorner,
closeCorner,
closeAllCorners
})
</script>
<style scoped lang="less">
.corner {
width: 260px;
position: absolute;
}
.top-left {
top: 10px;
left: 10px;
}
.top-right {
top: 10px;
right: 10px;
}
.bottom-left {
top: 170px;
left: 10px;
}
.bottom-right {
top: 170px;
right: 10px;
}
.content {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.title {
font-size: 16px;
padding: 8px;
border-bottom: 1px solid #ccc;
background-color: #fff;
}
.data-item {
display: flex;
margin-bottom: 4px;
font-size: 14px;
}
.label {
width: 55px;
flex-shrink: 0;
}
.value {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 关闭按钮样式 */
.close-btn {
position: absolute;
top: 10px;
right: 10px;
width: 14px;
color: #000;
font-size: 14px;
font-weight: bold;
cursor: pointer;
}
.indicator {
display: flex;
}
</style>

View File

@@ -0,0 +1,3 @@
import MtPreviewYPT from './index.vue'
export default MtPreviewYPT

View File

@@ -0,0 +1,878 @@
<template>
<div
:style="{ backgroundColor: useData.display ? '' : canvas_cfg.color }"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
@wheel="onMouseWheel"
>
<div
v-loading="useData.loading"
:style="canvasStyle"
:class="`canvasArea ${isDragging ? 'cursor-grabbing' : mtPreviewProps.canDrag ? 'cursor-grab' : ''} `"
>
<!-- <el-button type="primary" class="backBtn" @click="onBack" v-if="!useData.display">返回</el-button> -->
<!-- 缩放控制按钮 (默认注释需要时可开启) -->
<!-- <div class="zoom-controls">
<el-button icon="ZoomIn" size="small" @click="zoomIn"></el-button>
<el-button icon="ZoomOut" size="small" @click="zoomOut"></el-button>
<el-button icon="RefreshLeft" size="small" @click="resetTransform"></el-button>
</div>
-->
<!-- <el-scrollbar ref="elScrollbarRef" class="w-1/1 h-1/1" @scroll="onScroll" > -->
<div ref="canvasAreaRef">
<render-core
v-model:done-json="done_json"
:canvas-cfg="canvas_cfg"
:grid-cfg="grid_cfg"
:show-ghost-dom="false"
:canvas-dom="canvasAreaRef"
:global-lock="false"
:preivew-mode="true"
:show-popover="mtPreviewProps.showPopover"
@setDoneJson="setDoneJson"
@element-click="handleElementClick"
></render-core>
</div>
<drag-canvas
ref="dragCanvasRef"
:scale-ratio="canvas_cfg.scale"
@drag-canvas-mouse-down="dragCanvasMouseDown"
@drag-canvas-mouse-move="dragCanvasMouseMove"
@drag-canvas-mouse-up="dragCanvasMouseUp"
></drag-canvas>
</div>
</div>
<!-- 弹框 -->
<iframeDia :steadyState="dataList" ref="iframeDiaRef" @lineListChange="indicator"></iframeDia>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch, computed, onUnmounted, nextTick } from 'vue'
import RenderCore from '@/components/mt-edit/components/render-core/index.vue'
import type { IExportJson, IExportDoneJson } from '../mt-edit/components/types'
import { useExportJsonToDoneJson } from '../mt-edit/composables'
import type { IDoneJson } from '../mt-edit/store/types'
import { getItemAttr, previewCompareVal, setItemAttr } from '../mt-edit/utils'
import { ElScrollbar, ElMessage, ElMessageBox, ElButton } from 'element-plus'
import DragCanvas from '@/components/mt-edit/components/drag-canvas/index.vue'
import { useDataStore } from '@/stores/menuList'
import { globalStore } from '../mt-edit/store/global'
import type { IDoneJsonEventList } from '../mt-edit/store/types'
import IframeDia from './iframeDia.vue'
import MQTT from '@/utils/mqtt'
const iframeDiaRef = ref<any>(null)
const dataList = ref([])
// 节流函数实现 (替代lodash减少依赖)
const throttle = (func: (...args: any[]) => void, wait: number) => {
let lastTime = 0
return (...args: any[]) => {
const now = Date.now()
if (now - lastTime >= wait) {
func.apply(this, args)
lastTime = now
}
}
}
type MtPreviewProps = {
exportJson?: IExportJson
canZoom?: boolean
canDrag?: boolean
showPopover?: boolean
}
const mtPreviewProps = withDefaults(defineProps<MtPreviewProps>(), {
canDrag: true,
canZoom: true,
showPopover: true
})
const emits = defineEmits(['onEventCallBack'])
const useData = useDataStore()
const canvasAreaRef = ref<HTMLDivElement | null>(null)
const savedExportJson = ref<IExportJson>()
// 画布配置 - 扩展支持平移
const canvas_cfg = ref({
// width: 1920,
// height: 1080,
width: globalStore.canvasCfg.width,
height: globalStore.canvasCfg.height,
scale: 1,
color: '',
img: '',
guide: true,
adsorp: true,
adsorp_diff: 3,
transform_origin: {
x: 0,
y: 0
},
drag_offset: {
x: 0,
y: 0
},
// 平移属性
pan: {
x: 0,
y: 0
}
})
const grid_cfg = ref({
enabled: true,
align: true,
size: 10
})
const done_json = ref<IDoneJson[]>([])
const elScrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
const dragCanvasRef = ref<InstanceType<typeof DragCanvas>>()
const scroll_info = reactive({
begin_left: 0,
begin_top: 0,
left: 0,
top: 0
})
// 拖拽状态变量
const isDragging = ref(false)
const startPos = ref({ x: 0, y: 0 })
// 非响应式临时变量,减少响应式更新频率
const tempPan = { x: 0, y: 0 }
let animationFrameId: number | null = null
// 计算画布样式
const canvasStyle = computed(() => ({
transform: `translate(${canvas_cfg.value.pan.x}px, ${canvas_cfg.value.pan.y}px) scale(${canvas_cfg.value.scale})`,
transformOrigin: '0 0',
// width: `100vw`,
// height: `100vh`,
width: canvas_cfg.value.width + 'px',
height: canvas_cfg.value.height + 'px',
backgroundColor: useData.display ? '' : canvas_cfg.value.color,
backgroundImage: canvas_cfg.value.img ? `url(${canvas_cfg.value.img})` : 'none',
backgroundSize: '100% 100%',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
marginLeft: (document.documentElement.clientWidth - canvas_cfg.value.width * canvas_cfg.value.scale) / 2 + 'px'
}))
// 鼠标按下事件 - 开始拖拽
const onMouseDown = (e: MouseEvent) => {
if (mtPreviewProps.canDrag && e.button === 0) {
// 仅响应左键
e.preventDefault()
e.stopPropagation()
isDragging.value = true
startPos.value = {
x: e.clientX - canvas_cfg.value.pan.x,
y: e.clientY - canvas_cfg.value.pan.y
}
// 初始化临时位置
tempPan.x = canvas_cfg.value.pan.x
tempPan.y = canvas_cfg.value.pan.y
// 隐藏默认拖拽行为
if (canvasAreaRef.value) {
canvasAreaRef.value.style.userSelect = 'none'
}
// 启动动画帧同步
if (!animationFrameId) {
const syncPosition = () => {
if (isDragging.value) {
canvas_cfg.value.pan.x = tempPan.x
canvas_cfg.value.pan.y = tempPan.y
animationFrameId = requestAnimationFrame(syncPosition)
} else {
animationFrameId = null
}
}
animationFrameId = requestAnimationFrame(syncPosition)
}
}
}
// 节流处理鼠标移动事件 (16ms约等于60fps)
const throttledMouseMove = throttle((e: MouseEvent) => {
if (isDragging.value && mtPreviewProps.canDrag) {
e.preventDefault()
e.stopPropagation()
// 只更新临时变量,通过动画帧同步到响应式对象
tempPan.x = e.clientX - startPos.value.x
tempPan.y = e.clientY - startPos.value.y
}
}, 16)
// 鼠标移动事件 - 处理拖拽
const onMouseMove = throttledMouseMove
// 鼠标释放事件 - 结束拖拽
const onMouseUp = (e: MouseEvent) => {
if (isDragging.value) {
isDragging.value = false
// 恢复选择功能
if (canvasAreaRef.value) {
canvasAreaRef.value.style.userSelect = ''
}
// 取消动画帧
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
}
}
// 缩放控制函数
const zoomIn = () => {
if (mtPreviewProps.canZoom) {
canvas_cfg.value.scale = Math.min(canvas_cfg.value.scale + 0.1, 3) // 最大放大到3倍
}
}
const zoomOut = () => {
if (mtPreviewProps.canZoom) {
canvas_cfg.value.scale = Math.max(canvas_cfg.value.scale - 0.1, 0.3) // 最小缩小到0.3倍
}
}
// 重置变换
const resetTransform = () => {
// 定义重置变换的函数
canvas_cfg.value.scale = useData.display ? 1 : document.documentElement.clientHeight / globalStore.canvasCfg.height
canvas_cfg.value.pan = { x: 0, y: 0 }
tempPan.x = 0
tempPan.y = 0
}
// 鼠标滚轮事件 - 缩放
const onMouseWheel = (e: WheelEvent) => {
if (mtPreviewProps.canZoom) {
e.preventDefault()
e.stopPropagation()
// 计算缩放因子
const delta = e.deltaY < 0 ? 0.1 : -0.1
const newScale = Math.max(0.3, Math.min(3, canvas_cfg.value.scale + delta))
// 如果缩放中心是鼠标位置,计算新的平移值以保持视觉中心
if (canvasAreaRef.value) {
const rect = canvasAreaRef.value?.getBoundingClientRect()
const mouseX = e.clientX - rect.left
const mouseY = e.clientY - rect.top
// 计算缩放前后的鼠标位置差异,调整平移量
const scaleRatio = newScale / canvas_cfg.value.scale
canvas_cfg.value.pan.x = mouseX - (mouseX - canvas_cfg.value.pan.x) * scaleRatio
canvas_cfg.value.pan.y = mouseY - (mouseY - canvas_cfg.value.pan.y) * scaleRatio
// 同步到临时变量
tempPan.x = canvas_cfg.value.pan.x
tempPan.y = canvas_cfg.value.pan.y
// 应用新缩放值
canvas_cfg.value.scale = newScale
}
}
}
const dragCanvasMouseDown = () => {
scroll_info.begin_left = scroll_info.left
scroll_info.begin_top = scroll_info.top
}
const dragCanvasMouseMove = (move_x: number, move_y: number) => {
let new_left = scroll_info.begin_left - move_x
let new_top = scroll_info.begin_top - move_y
elScrollbarRef.value?.setScrollLeft(new_left)
elScrollbarRef.value?.setScrollTop(new_top)
}
/**
* 画布拖动结束事件
*/
const dragCanvasMouseUp = () => {}
const setItemAttrByID = (id: string, key: string, val: any) => {
return setItemAttr(id, key, val, done_json.value)
}
const setItemAttrs = (info: { id: string; key: string; val: any }[]) => {
info.forEach(f => {
setItemAttr(f.id, f.key, f.val, done_json.value)
})
}
const getItemAttrByID = (id: string, key: string, val: any) => {
return getItemAttr(id, key, done_json.value)
}
const setItemAttrByIDAsync = (id: string, key: string, val: any) => {
// 通过改变属性的事件去设置值时 需要转换成宏任务 不然多个事件判断会有问题
setTimeout(() => {
setItemAttrByID(id, key, val)
}, 0)
}
;(window as any).$mtElMessage = ElMessage
;(window as any).$mtElMessageBox = ElMessageBox
;(window as any).$setItemAttrByID = (id: string, key: string, val: any) => setItemAttrByIDAsync(id, key, val)
;(window as any).$getItemAttrByID = getItemAttrByID
;(window as any).$previewCompareVal = previewCompareVal
;(window as any).$mtEventCallBack = (type: string, item_id: string, ...args: any[]) =>
emits('onEventCallBack', type, item_id, ...args)
onMounted(async () => {
// 启动消息监听 iframe传过来的参数
receiveMessage()
if (mtPreviewProps.exportJson) {
await setImportJson(mtPreviewProps.exportJson)
}
// 连接mqtt
await setMqtt()
// await sendTableData()
})
// mqtt推过来的 lineId
let keyList = ref<any>([])
// 实时数据表格
const tableData = ref()
const sendTableData = () => {
try {
// 类型检查,确保 tableData.value 是数组
if (!Array.isArray(tableData.value)) {
console.warn('tableData is not an array, current value:', tableData.value)
return
}
// 确保只传输可序列化的数据
const cleanData = tableData.value.map(item => ({
name: item.name,
date: item.timeId,
address: item.eventDesc
}))
window.parent.postMessage(
{
action: 'securityDetailData',
data: cleanData
},
'*'
)
} catch (error) {
console.error('数据传输失败:', error)
}
}
let transmissionDeviceIds: string[] = []
let eventListAll = ref<any>([])
const receiveMessage = () => {
// 在 iframe 内的页面中
window.addEventListener('message', function (event) {
// 验证消息来源(在生产环境中应该验证 origin
// if (event.origin !== 'trusted-origin') return;
const { type, payload } = event.data
if (type === 'RESET_EVENT') {
// 处理复位事件
handleResetEvent()
} else if (type === 'ANALYSIS_KEYS') {
// 处理 ANALYSIS_KEYS 消息
// 在处理新数据前,先清理现有的动态文字
if (savedExportJson.value) {
savedExportJson.value.json =
savedExportJson.value.json?.filter(item => !item.id?.startsWith('auto-text-')) || []
done_json.value = done_json.value.filter(item => !item.id?.startsWith('auto-text-'))
}
init()
}
// 对于其他类型的消息,我们仍然调用 init()
else if (type) {
init()
}
})
}
// 复位事件处理函数
const handleResetEvent = () => {
// 清空或重置 表格数据
// if (tableData.value && Array.isArray(tableData.value)) {
// tableData.value = []
// } else {
// // 确保 tableData 是一个空数组
// tableData.value = []
// }
// 接线图数据
keyList.value = []
setTimeout(() => {
// 表格数据
sendTableData()
// 接线图数据
if (savedExportJson.value) {
setImportJson(savedExportJson.value)
}
}, 100)
console.log('执行复位操作完成')
}
//根据 lineId 查找传输设备 ID 的函数
const findTransmissionDeviceIdsByKeyList = (array: any) => {
if (!savedExportJson.value?.json) return []
const deviceIds = savedExportJson.value.json.filter(item => array.includes(item.lineId ?? '')).map(item => item.id)
// console.log('传输设备 ID 列表:', deviceIds)
return deviceIds
}
// 为每个图元动态添加点击事件
const addClickEventsToElements = () => {
if (savedExportJson.value && savedExportJson.value.json) {
savedExportJson.value.json.forEach(item => {
// 检查是否已经有点击事件
const hasClickEvent = item.events && item.events.some(event => event.type === 'click')
if (!hasClickEvent) {
// 创建点击事件对象
const clickEvent: IDoneJsonEventList = {
id: generateRandomId(), // 生成随机ID
type: 'click', // 明确指定为字面量类型
action: 'customCode', // 自定义操作
jump_to: '',
change_attr: [],
custom_code: '', // 可以在这里添加自定义代码
trigger_rule: {
value: null
}
}
// 如果图元还没有events数组则创建一个
if (!item.events) {
item.events = []
}
// 添加点击事件
item.events.push(clickEvent)
}
})
}
}
// 生成随机ID的辅助函数
const generateRandomId = () => {
return Math.random().toString(36).substr(2, 10)
}
const setImportJson = (exportJson: IExportJson) => {
// 保存exportJson供后续使用
savedExportJson.value = exportJson
// 定义要执行的操作函数
const executeOperations = () => {
const { canvasCfg, gridCfg, importDoneJson } = useExportJsonToDoneJson(savedExportJson.value)
// 保留现有的平移和缩放设置
const currentPan = canvas_cfg.value.pan
const currentScale = canvas_cfg.value.scale
canvas_cfg.value = {
...canvasCfg,
pan: currentPan,
scale: currentScale
}
grid_cfg.value = gridCfg
done_json.value = importDoneJson
// 为图元添加点击事件
addClickEventsToElements()
// 首页初始化的时候
setTimeout(() => {
done_json.value.forEach(item => {
//报警设备闪烁
if (findTransmissionDeviceIdsByKeyList(keyList.value).includes(item.id)) {
item.props.fill.val = 'red'
item.common_animations.val = 'flash'
} else {
item.common_animations.val = ''
}
})
}, 1000)
}
if (!useData.loading) {
if (exportJson == null) {
setDoneJson(useData.dataTree[0].kId)
publish(useData.dataTree[0])
} else {
executeOperations()
}
} else {
// 如果不是true添加监听
const stopWatch = watch(
() => useData.loading,
newVal => {
if (newVal === false) {
// 当loading变为true时执行操作
if (exportJson == null) {
setDoneJson(useData.dataTree[0].kId)
publish(useData.dataTree[0])
} else {
executeOperations()
}
// 执行后停止监听
stopWatch()
}
}
)
}
return true
}
// 添加一个新的 ref 来存储当前点击的设备ID
const currentClickedElementId = ref<string | null>(null)
// 图纸的kId
let storedSelectedId = ''
storedSelectedId = localStorage.getItem('selectedId') || ''
// 当前点击的元素lineId 通过mt-edit/render-core/index.vue传过来的click事件
const handleElementClick = (elementId: string, lineName: string) => {
iframeDiaRef.value.showNextCorner(elementId, lineName)
// 保存当前点击的设备ID
// indicator(['00B78D0171091', '00B78D0171092'])
// currentClickedElementId.value = elementId
// const item = done_json.value.find(item => item.lineId === elementId)
// if (item && item.events && item.events.some(event => event.type === 'click')) {
// // 发送设备ID到父级iframe
// window.parent.postMessage(
// {
// action: 'coreClick',
// coreId: elementId.toString(), // 确保是字符串
// selectedId: storedSelectedId // 新增的字段
// // elementData: item // 可以发送整个元素数据
// },
// '*'
// )
// }
}
const searchDevicesConnect = (transmissionDeviceIds: string[]) => {
// 确保 savedExportJson.value 存在
if (!savedExportJson.value?.json) {
console.warn('savedExportJson.value 或 json 未定义')
return []
}
// 查找所有连线元素
const lineElements = savedExportJson.value.json.filter(item => item.type === 'sys-line' && item.props?.bind_anchors)
// 查找所有开关元素
const switchElements = savedExportJson.value.json.filter(item => item.title?.includes('开关'))
// 存储连接线的ID
const connectedLineIds: string[] = []
// 存储找到的开关ID
const switchIds: string[] = []
// 首先找出所有传输设备连接的开关
for (const deviceId of transmissionDeviceIds) {
for (const line of lineElements) {
const bindAnchors = line.props.bind_anchors as { start?: { id: string }; end?: { id: string } } | undefined
if (!bindAnchors) continue
const startId = bindAnchors.start?.id
const endId = bindAnchors.end?.id
// 检查连线是否连接传输设备和开关
if (startId === deviceId && switchElements.some(s => s.id === endId)) {
if (endId && !switchIds.includes(endId)) {
switchIds.push(endId)
}
if (!connectedLineIds.includes(line.id!)) {
connectedLineIds.push(line.id!)
}
} else if (endId === deviceId && switchElements.some(s => s.id === startId)) {
if (startId && !switchIds.includes(startId)) {
switchIds.push(startId)
}
if (!connectedLineIds.includes(line.id!)) {
connectedLineIds.push(line.id!)
}
}
}
}
// 然后找出开关之间的连线
for (const line of lineElements) {
const bindAnchors = line.props.bind_anchors as { start?: { id: string }; end?: { id: string } } | undefined
if (!bindAnchors) continue
const startId = bindAnchors.start?.id
const endId = bindAnchors.end?.id
// 检查连线是否连接两个开关
if (startId && endId && switchIds.includes(startId) && switchIds.includes(endId)) {
if (!connectedLineIds.includes(line.id!)) {
connectedLineIds.push(line.id!)
}
}
}
// console.log('连接的连线ID列表:', connectedLineIds)
return connectedLineIds
}
// 预览时候绑定指标等的点击事件
const setDoneJson = async (kId: string) => {
const filteredItems = useData.dataTree.filter(item => item.kId == kId)
if (filteredItems.length === 0) {
console.error(`No item found with kId: ${kId}`)
return
}
const item = filteredItems[0]
// 根据传过来的kId找到所在的id
const foundId = item.id
if (foundId) {
// 将当前选中的行数据存储到本地存储
localStorage.setItem('selectedId', foundId)
}
storedSelectedId = localStorage.getItem('selectedId') || ''
if (!item.path) {
console.error(`Item with kId: ${kId} does not have a path property`)
return
}
setImportJson(JSON.parse(item.path))
}
const init = async () => {
setTimeout(() => {
// 执行动态添加文本的操作
// const updatedDoneJson = addTextNextToTransport()
// 调用函数获取传输设备 ID
transmissionDeviceIds = findTransmissionDeviceIdsByKeyList(keyList.value)
// 重新设置导入的JSON以触发界面更新
setImportJson(savedExportJson.value)
}, 100)
}
const timer = ref()
// 连接mqtt
const mqttClient = ref()
const setMqtt = async () => {
mqttClient.value = new MQTT('/zl/TemperData/#')
// 设置消息接收回调
try {
await mqttClient.value.init()
// 订阅主题
await mqttClient.value.subscribe('/zl/TemperData/#') //实时数据
await mqttClient.value.subscribe('/zl/csConfigRtData/#') //指标
// 设置消息接收回调
mqttClient.value.onMessage((subscribe: string, message: any) => {
const msg: any = uint8ArrayToObject(message)
// console.log('🚀 ~ setMqtt ~ msg:', msg)
if (subscribe.split('/')[2] === 'csConfigRtData') {
// 指标数据
dataList.value = JSON.parse(msg.message)
//console.log('🚀 ~ setMqtt ~ dataList:', dataList.value)
// keyList.value = JSON.parse(list.path).canvasCfg.lineList
}
if (subscribe.split('/')[2] === 'TemperData') {
// 表格数据
tableData.value = []
tableData.value = JSON.parse(msg.message)
//console.log('🚀 ~ setMqtt ~ tableData:', tableData.value)
// 闪烁点
// if (Array.isArray(tableData.value) && tableData.value.length > 0) {
// // 提取所有的 id 并去重(使用 Set 方式,性能更好)
// const uniqueIds = [
// ...new Set(
// tableData.value
// .filter(item => item.id) // 确保 id 存在
// .map(item => item.id) // 提取 id
// )
// ]
// keyList.value = uniqueIds
// console.log('提取的唯一ID列表:', keyList.value)
// }
sendTableData()
}
})
} catch (error) {
console.error('MQTT 初始化失败:', error)
}
}
const publish = async (list: any) => {
console.log('🚀 ~ publish ~ list:', JSON.parse(list.path).canvasCfg.lineList)
if (mqttClient.value) {
// 发送消息
await mqttClient.value.publish(
'/zl/askCSConfigWarnData/' + storedSelectedId,
`[${JSON.parse(list.path).canvasCfg.lineList}]`
)
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
timer.value = setInterval(
() => {
mqttClient.value.publish(
'/zl/askCSConfigWarnData/' + storedSelectedId,
`[${JSON.parse(list.path).canvasCfg.lineList}]`
)
},
3 * 60 * 1000
)
}
}
// 绑定指标
const indicator = async (ids: string[]) => {
if (mqttClient.value) {
// await mqttClient.value.subscribe('zl/askCSConfigRtData/' + useData.dataTree[0].id)
// 发送消息
await mqttClient.value.publish('/zl/askCSConfigRtData/' + storedSelectedId, `[${ids}]`)
}
}
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
// 方法1: 使用 mqtt消息转换
function uint8ArrayToObject(uint8Array: Uint8Array) {
try {
// 将 Uint8Array 解码为字符串
const decoder = new TextDecoder('utf-8')
const jsonString = decoder.decode(uint8Array)
// 将 JSON 字符串解析为对象
return JSON.parse(jsonString)
} catch (error) {
console.error('转换失败:', error)
return null
}
}
const onBack = () => {
window.close()
}
watch(
() => useData.display,
newVal => {
if (newVal) {
canvas_cfg.value.scale = 1
canvas_cfg.value.pan = {
x: 0,
y: 0
}
} else {
canvas_cfg.value.scale = document.documentElement.clientHeight / globalStore.canvasCfg.height
}
},
{ immediate: true }
)
defineExpose({
setItemAttrByID,
setImportJson,
setItemAttrs,
zoomIn,
zoomOut,
resetTransform
})
</script>
<style scoped>
.canvasArea {
position: relative;
/* 移除过渡效果,避免拖拽延迟 */
will-change: transform;
/* 提示浏览器优化transform属性 */
}
.backBtn {
position: absolute;
top: 20px;
right: 10px;
z-index: 2;
}
.zoom-controls {
position: fixed;
top: 20px;
left: 20px;
z-index: 1;
display: flex;
gap: 5px;
}
.cursor-grab {
cursor: grab;
}
.cursor-grabbing {
cursor: grabbing;
}
.el-table {
/* --el-table-border-color: #0a73ff;
--el-table-row-hover-bg-color: #3d4862;
--el-table-header-bg-color: #2a3b62;
--el-table-bg-color: #343849c7; */
--el-table-border-color: #0a73ff;
--el-table-row-hover-bg-color: #0a73ff20;
--el-table-header-bg-color: #0a73ff40;
--el-table-bg-color: #ffffff00;
text-align: center;
color: #ffffff;
}
:deep(.el-table tr) {
/* background-color: #242936; */
background-color: #00000090 !important;
}
:deep(.el-table .cell) {
color: #ffffff;
text-align: center;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell) {
/* background-color: #303b54; */
background-color: #5aa1ff29;
}
/* .aaaa{
position: absolute;
right: 0;
top: 0px;
z-index: 111123;
height: 100px;
width: 100px;
background-color: #ccc;
} */
</style>

View File

@@ -0,0 +1,3 @@
import MtPreview from './index.vue'
export default MtPreview

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,11 @@
v-loading="useData.loading"
element-loading-background="#343849c7"
:style="{ backgroundColor: useData.display ? '' : canvas_cfg.color }"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
@wheel="onMouseWheel"
>
<el-button type="primary" class="backBtn" @click="onBack" v-if="!useData.display">返回</el-button>
<!-- 缩放控制按钮 (默认注释需要时可开启) -->
@@ -18,11 +23,6 @@
ref="canvasAreaRef"
:class="`canvasArea ${isDragging ? 'cursor-grabbing' : mtPreviewProps.canDrag ? 'cursor-grab' : ''} `"
style=""
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseUp"
@wheel="onMouseWheel"
:style="canvasStyle"
>
<render-core
@@ -92,6 +92,8 @@ const tableData = [
// }
]
const targetKeywords = ['开关', '器', '阀门', '控制']
const showDetail = ref(false)
const showDetailClick = () => {
@@ -581,11 +583,12 @@ const setImportJson = (exportJson: IExportJson) => {
addClickEventsToElements()
// 首页初始化的时候
setTimeout(() => {
nextTick(() => {
done_json.value.forEach(item => {
//报警设备闪烁
if (findTransmissionDeviceIdsByKeyList(list.value).includes(item.id)) {
// item.props.fill.val = '#fcfc57'
item.props.fill.val = sendColor.value
item.common_animations.val = 'flash'
if (findTransmissionDeviceIdsByKeyList(listMax.value).includes(item.id)) {
@@ -644,7 +647,7 @@ const setImportJson = (exportJson: IExportJson) => {
// item.props.ani_color.val = '#8c0ae2'
// }
})
}, 1000)
})
}
if (!useData.loading) {
@@ -691,7 +694,7 @@ const findSwitchByLineEndpoint = (lineId: string): string | null => {
const startId = bindAnchors.start?.id
if (startId) {
const startElement = savedExportJson.value.json.find(item => item.id === startId)
if (startElement && startElement.title?.includes('开关')) {
if (startElement && targetKeywords.some(keyword => startElement.title?.includes(keyword))) {
return startElement.id!
}
}
@@ -700,7 +703,7 @@ const findSwitchByLineEndpoint = (lineId: string): string | null => {
const endId = bindAnchors.end?.id
if (endId) {
const endElement = savedExportJson.value.json.find(item => item.id === endId)
if (endElement && endElement.title?.includes('开关')) {
if (endElement && targetKeywords.some(keyword => endElement.title?.includes(keyword))) {
return endElement.id!
}
}
@@ -777,6 +780,7 @@ const handleElementClick = (elementId: string) => {
)
}
}
const bindList = ref<string[]>([])
const searchDevicesConnect = (transmissionDeviceIds: string[]) => {
// 确保 savedExportJson.value 存在
if (!savedExportJson.value?.json) {
@@ -786,9 +790,23 @@ const searchDevicesConnect = (transmissionDeviceIds: string[]) => {
// 查找所有连线元素
const lineElements = savedExportJson.value.json.filter(item => item.type === 'sys-line' && item.props?.bind_anchors)
bindList.value = [
...new Set(
lineElements
.map(item => {
return [item.props?.bind_anchors.start?.id, item.props?.bind_anchors.end?.id]
})
.flat()
)
]
// 查找所有开关元素
const switchElements = savedExportJson.value.json.filter(item => item.title?.includes('开关'))
const switchElements = savedExportJson.value.json.filter(item =>
targetKeywords.some(keyword => item.title?.includes(keyword))
)
// const switchElements = savedExportJson.value.json.filter(item =>
// bindList.value.some(keyword => item.id?.includes(keyword) && item.lineId=='')
// )
// 存储连接线的ID
const connectedLineIds: string[] = []
@@ -1010,16 +1028,26 @@ const setMqtt = async () => {
await mqttClient.value.subscribe('/zl/rtData/#')
// 设置消息接收回调
mqttClient.value.onMessage((subscribe: string, message: any) => {
mqttClient.value.onMessage(async (subscribe: string, message: any) => {
const msg: any = uint8ArrayToObject(message)
console.log('🚀 ~ 接受消息:', msg)
setTimeout(() => {
list.value = [...new Set(msg.filter((item: any) => item.devStatus === 1).map((item: any) => item.lineId))]
sendColor.value = '#ff0000'
// await setImportJson(savedExportJson.value)
await setTimeout(() => {
done_json.value.forEach(item => {
msg.forEach((msgValue: any) => {
if (item.id == msgValue.id) {
item.props.text.val = item.props.text.val.replace(/#{3}/g, msgValue.value) //'B相负载电流-CP95:31'
}
})
list.value.forEach((listValue: any) => {
if (listValue == item.lineId && item.type == 'svg') {
item.props.fill.val = '#ff0000'
// item.common_animations.val = 'flash'
}
})
})
}, 100)
})

View File

@@ -7,5 +7,7 @@ import '@/components/mt-edit/assets/css/custom_ani.css'
import MtDzr from '@/components/mt-dzr'
import MtEdit from '@/components/mt-edit'
import MtPreview from '@/components/mt-preview'
import MtPreviewYpt from '@/components/mt-preview-ypt'
import MtPreviewZl from '@/components/mt-preview-zl'
import { leftAsideStore } from '@/components/mt-edit/store/left-aside'
export { MtDzr, MtEdit, MtPreview, leftAsideStore }
export { MtDzr, MtEdit, MtPreview, leftAsideStore, MtPreviewYpt, MtPreviewZl }

View File

@@ -19,6 +19,18 @@ export const constantRoutes = [
path: '/preview',
component: () => import('../views/preview/index.vue')
},
{
// 云平台预览页面
name: 'preview_YPT',
path: '/preview_YPT',
component: () => import('../views/preview/index_YPT.vue')
},
{
// 云平台预览页面
name: 'preview_ZL',
path: '/preview_ZL',
component: () => import('../views/preview/index_ZL.vue')
},
{
name: 'edit-load',
path: '/edit-load',

View File

@@ -11,6 +11,7 @@ export const useDataStore = defineStore('data-store', {
identifying: '0',
mqttID: '',
preview: '',
keyName: '', //选中的name
wxqr: '',
loading: true,
display: false, //无锡项目进去是true其他项目是false 控制预览的时候返回按钮的展示
@@ -67,13 +68,18 @@ export const useDataStore = defineStore('data-store', {
modify(kId: number, val: string) {
this.dataTree.forEach((item: any) => {
if (item.kId == kId) {
item.name = val
}
})
},
// 放置kid
placeKid(id: String) {
this.identifying = id
this.keyName = ''
this.dataTree.forEach((item: any) => {
if (item.kId == id) {
this.keyName = item.name
}
})
},
setUpPath(data: String) {
this.dataTree.forEach((item: any) => {

View File

@@ -9,6 +9,7 @@ export interface DataStoreState {
preview?: String
wxqr?: String
loading?: boolean
keyName?: String
display?: Boolean //是否展示返回按钮
graphicDisplay?: string //是否展示数据绑定图元
}

View File

@@ -25,14 +25,15 @@ interface SvgConfig {
}
}
// 处理单个SVG元素的函数
const processSvgItem = async (item: ElementItem): Promise<SvgConfig> => {
const processSvgItem = async (item: ElementItem, type: string): Promise<SvgConfig> => {
try {
const svgContent = await download({ filePath: item.path })
// 替换填充色用于缩略图
const filledSvg = svgContent.replace(/(\sfill=(["']))[^"']*(\2)/g, '$1#000000$3')
const filledSvg =
type == '特殊图元' ? svgContent : svgContent.replace(/(\sfill=(["']))[^"']*(\2)/g, '$1#000000$3')
// 移除原始填充色
const cleanSvg = svgContent.replace(/\sfill=(["'])[^"']*\1/g, '')
const cleanSvg = type == '特殊图元' ? svgContent : svgContent.replace(/\sfill=(["'])[^"']*\1/g, '')
return {
id: item.id,
@@ -142,7 +143,7 @@ const loadSvg = async () => {
// 处理每个类型的元素并注册
for (const [type, items] of Object.entries(groupedElements)) {
const svgConfigs = await Promise.all(items.map(item => processSvgItem(item)))
const svgConfigs = await Promise.all(items.map(item => processSvgItem(item, type)))
leftAsideStore.registerConfig(type, svgConfigs)
}
} catch (error) {

View File

@@ -52,11 +52,11 @@ class MQTT {
}
try {
// const mqttUrl = 'ws://192.168.1.103:8083/mqtt'
const mqttUrl =
localStorage.getItem('MqttUrl') == 'null'
? 'ws://192.168.1.24:8085/mqtt'
: localStorage.getItem('MqttUrl')
const mqttUrl = 'ws://192.168.1.103:8083/mqtt'
// const mqttUrl =
// localStorage.getItem('MQTTZUTAI') == 'null'
// ? 'ws://192.168.1.103:8083/mqtt'
// : localStorage.getItem('MQTTZUTAI')
console.log('🚀 ~ MQTT ~ init ~ mqttUrl:', mqttUrl)
this.client = mqtt.connect(mqttUrl, this.defaultOptions as IClientOptions)
this.setupEventListeners()

View File

@@ -20,13 +20,7 @@ class HttpRequest {
config => {
// 添加全局的loading..
// config.headers['Authorization'] =
// 'bearer ' + JSON.parse(window.localStorage.getItem('adminInfo') || '{}').access_token; // 请求头带上token token要在登录的时候保存在localStorage中
// console.log(
// "🚀 ~ requestHandler ~ config.headers['Authorization']:",
// JSON.parse(window.localStorage.getItem('adminInfo') || '{}'),
// config.headers
// );
// 'bearer ' + JSON.parse(window.localStorage.getItem('adminInfo') || '{}').access_token // 请求头带上token token要在登录的时候保存在localStorage中
config.headers['Authorization'] =
'bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5kZXgiOiJmYTM3YjkzY2M5MGQ0YzE3ODRjYThmNmRlYmRkZWUxYSIsInVzZXJfbmFtZSI6InJvb3QiLCJzY29wZSI6WyJhbGwiXSwibmlja25hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJ1c2VyVHlwZSI6MCwiZGVwdEluZGV4IjoiNTY5OWU1OTE2YTE4YTYzODFlMWFjOTJkYTViZDI2MjgiLCJleHAiOjE4MjE4MTc2MTksImF1dGhvcml0aWVzIjpbInJvb3QiXSwianRpIjoiMmJiM2Q5ZTYtNmY3Yy00Yjg1LThiM2EtZDI2ODdmMTUzMDg5IiwiY2xpZW50X2lkIjoibmpjbnRlc3QiLCJoZWFkU2N1bHB0dXJlIjoicmVzb3VyY2VEYXRhLzMxNzRDRUFFOUQ0MjRGMjJCQjkxQTU4OURENjdCMDUxLmpwZyJ9.WjeYl1lvvJdDE1FUGIhS99rE5qKaBXOypWxmxK0svWweGqEbu1XCLjKm_YkiTwjZJ_oIcn5JOO9rvHFkkea76BUsYo5wlzuBBiy7sKqM1fFzOFQq6hdFevNTJAbYH9FiBxYxI-e9DZ5mvLGE6umOjUfn_FAsku2w6Uj5DtvpOKBWYzLEPTEifOqNI9he4zJAmVZniUUMf26SDoEdfu0TyrIS1j_qKaEb-cqR1XDhivdthEBK5m9vxJyXFZ5kofNxwQQkit_oiqJRkCZIt9TWAjCh-frzMHCvA30hkAr-VCD2JfCmmEr3hW_lmwfINaPtFVbHCdCKqdrl6VmF1HObaQ'
// 请求头携带token

View File

@@ -0,0 +1,19 @@
<template>
<mt-preview-ypt ref="MtPreviewRef" @on-event-call-back="onEventCallBack"></mt-preview-ypt>
</template>
<script setup lang="ts">
import { MtPreviewYpt } from '@/export'
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
const MtPreviewRef = ref<InstanceType<typeof MtPreviewYpt>>()
const onEventCallBack = (type: string, item_id: string) => {
console.log(type, item_id)
if (type == 'test-dialog') {
ElMessage.success(`获取到了id:${item_id}`)
}
}
onMounted(() => {
MtPreviewRef.value?.setImportJson(JSON.parse(sessionStorage.getItem('exportJson') as any))
})
</script>

View File

@@ -0,0 +1,19 @@
<template>
<mt-preview-zl ref="MtPreviewRef" @on-event-call-back="onEventCallBack"></mt-preview-zl>
</template>
<script setup lang="ts">
import { MtPreviewZl } from '@/export'
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
const MtPreviewRef = ref<InstanceType<typeof MtPreviewZl>>()
const onEventCallBack = (type: string, item_id: string) => {
console.log(type, item_id)
if (type == 'test-dialog') {
ElMessage.success(`获取到了id:${item_id}`)
}
}
onMounted(() => {
MtPreviewRef.value?.setImportJson(JSON.parse(sessionStorage.getItem('exportJson') as any))
})
</script>