Files
pqs-9100_client/frontend/src/views/home/components/channelPairing.vue
2025-10-15 08:49:11 +08:00

556 lines
20 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>
<div class="flow-container" style="overflow: hidden; position: relative"
:style="{ height: vueFlowElement + 'px' }">
<!-- <el-button @click="logConnections">打印当前配对</el-button> -->
<VueFlow :nodes="nodes" :edges="edges" :connection-radius="30" :nodes-draggable="false" :dragging="false"
:zoom-on-scroll="false" :pan-on-drag="false" :disable-zoom-pan-on-connect="true"
:prevent-scrolling="true" :fit-view="true" :min-zoom="1" :max-zoom="1"
:nodesConnectable="checkStore.nodesConnectable" :elements-selectable="false" auto-connect
@connect="handleConnect" @connect-start="handleConnectStart" @connect-end="handleConnectEnd"
@pane-ready="onPaneReady" v-on:pane-mouse-move="false"></VueFlow>
</div>
<!-- 底部操作按钮 -->
<!-- <template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button>
</div>
</template> -->
<!-- 手动检测-勾选检测项弹窗 -->
<!-- <SelectTestItemPopup ref="selectTestItemPopupRef" @openTestDialog="openTestDialog"></SelectTestItemPopup> -->
</div>
</template>
<script lang="ts" setup>
import { h, ref, onMounted, PropType } from 'vue'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { dialogBig } from '@/utils/elementBind'
import { Platform, Flag } from '@element-plus/icons-vue'
import { Device } from '@/api/device/interface/device'
import { StandardDevice } from '@/api/device/interface/standardDevice'
import SelectTestItemPopup from '@/views/home/components/selectTestItemPopup.vue'
import { ElMessage, stepProps } from 'element-plus'
import CustomEdge from './RemoveableEdge.vue' // 导入自定义连接线组件
import { jwtUtil } from '@/utils/jwtUtil'
import { useCheckStore } from '@/stores/modules/check'
const vueFlowElement = ref(442)
const checkStore = useCheckStore()
const dialogVisible = ref(false)
const selectTestItemPopupRef = ref<InstanceType<typeof SelectTestItemPopup>>()
const testPopup = ref()
const dialogTitle = ref('手动检测')
const prop = defineProps({
devIdList: {
type: Array as any,
default: []
},
pqStandardDevList: {
type: Array as any,
default: []
},
planIdKey: {
type: String,
default: ''
},
deviceMonitor: {
type: Map as PropType<Map<string, any[]>>,
default: () => new Map()
}
})
// 计算对话框高度
const dialogHeight = ref(600)
// 初始化 VueFlow注册自定义连线类型
const { edges, setViewport } = useVueFlow({
edgeTypes: {
default: CustomEdge
}
})
// 初始化时锁定画布位置
const onPaneReady = () => {
setViewport({ x: 0, y: 0, zoom: 1 })
}
// 提取公共的label渲染函数
const createLabel = (text: string, type: string, Key: number) => {
return h(
'div',
{
style: {
display: 'flex',
// flexDirection: 'column',
alignItems: 'center',
fontSize: '15px',
height: '75px',
// textAlign: 'center',
border: '1px solid #ccc',
borderRadius: '8px',
padding: '8px',
backgroundColor: '#f9f9f9'
}
},
[
// h(Platform, {
// style: {
// height: '40px',
// marginBottom: '4px',
// color: '#526ade'
// }
// }),
h('img', {
src:
Key == 2
? new URL('@/assets/images/inspected1.jpg', import.meta.url).href
: new URL('@/assets/images/inspected2.png', import.meta.url).href,
// alt: '设备图标',
style: {
width: '50px',
marginRight: '5px'
// 保持原有的颜色风格,如果需要可以调整滤镜
// filter: 'invert(35%) sepia(65%) saturate(300%) hue-rotate(210deg)'
}
}),
h('div', { style: { textAlign: 'left' } }, ['设备名称:' + text, h('br'), '设备类型:' + type])
// h('div', null, '设备名称:' + text),
// h('div', null, '设备类型:' + type)
]
) as any
}
const createLabel3 = (text: string) => {
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
fontSize: '15px',
height: '35px',
textAlign: 'center',
border: '1px solid #ccc',
borderRadius: '8px',
// padding: '8px',
backgroundColor: '#f9f9f9'
}
},
[
h('div', {
style: {
width: '8px',
marginRight: '4px',
color: '#526ade'
}
}),
text
]
) as any
}
const handleConnectStart = (params: any) => {
onPaneReady()
}
const handleConnectEnd = (params: any) => {
onPaneReady()
}
const handleConnect = (params: any) => {
const sourceNode = nodes.value.find(node => node.id === params.source)
const targetNode = nodes.value.find(node => node.id === params.target)
// 连接规则验证
const isValidConnection = sourceNode?.type === 'input' && targetNode?.type === 'output'
if (!isValidConnection) {
removeEdge(params)
ElMessage.warning('只能从被检通道连接到标准通道')
return
}
// 过滤掉当前连接,检查是否还有重复的
const existingEdges = edges.value.filter(edge => edge.source === params.source || edge.target === params.target)
// 如果同源或同目标的连接超过1个说明有重复
if (existingEdges.length > 1) {
const duplicateSource = existingEdges.filter(edge => edge.source === params.source).length > 1
const duplicateTarget = existingEdges.filter(edge => edge.target === params.target).length > 1
if (duplicateSource) {
removeEdge(params)
ElMessage.warning('该被检通道已经连接,不能重复连接')
return
}
if (duplicateTarget) {
removeEdge(params)
ElMessage.warning('该标准通道已经连接,不能重复连接')
return
}
}
}
// 删除不合法连接
const removeEdge = (params: any) => {
const edgeIndex = edges.value.findIndex(edge => edge.source === params.source && edge.target === params.target)
if (edgeIndex !== -1) {
edges.value.splice(edgeIndex, 1)
}
}
const nodes = ref([])
const planId = ref('')
const devIds = ref<string[]>()
const standardDevIds = ref<string[]>()
const open = async () => {
edges.value = []
devIds.value = prop.devIdList.map(d => d.id)
standardDevIds.value = prop.pqStandardDevList.map(d => d.id)
planId.value = prop.planIdKey
nodes.value = createNodes(prop.devIdList, prop.pqStandardDevList, prop.deviceMonitor)
dialogVisible.value = true
onPaneReady()
}
onMounted(() => {
open()
})
const handleNext = async () => {
if (edges.value.length === 0) {
ElMessage.warning('请先完成通道配对')
return false
}
// const sourceKey = edge.source.replace('被检通道-', '').replace('-', '_');
let chnNumList: string[] = []
await edges.value.forEach(edge => {
const match = edge.source.split('-')
if (match) {
chnNumList.push(match[2])
}
})
const connections = edges.value.reduce(
(map, edge) => {
// 从source中提取设备ID和通道号: 被检通道-{deviceId}-{channelNum} => {deviceId}-{channelNum}
const sourceKey = edge.source.replace('被检通道-', '').replace('-', '_')
// 从target中提取设备ID和通道号: 标准通道-{deviceId}-{channelNum} => {deviceId}-{channelNum}
const targetValue = edge.target.replace('标准通道-', '').replace('-', '_')
map[sourceKey] = targetValue
return map
},
{} as Record<string, string>
)
await generateChannelMapping()
await checkStore.setChnNum(chnNumList)
return {
title: dialogTitle.value,
mapping: channelMapping.value,
plan: planId.value,
login: jwtUtil.getLoginName(),
devIdsArray: devIds.value,
standardDevIdsArray: standardDevIds.value,
pair: connections
}
}
const openTestDialog = async () => {
// 转换连接信息只保留设备ID和通道号
const connections = edges.value.reduce(
(map, edge) => {
// 从source中提取设备ID和通道号: 被检通道-{deviceId}-{channelNum} => {deviceId}-{channelNum}
const sourceKey = edge.source.replace('被检通道-', '').replace('-', '_')
// 从target中提取设备ID和通道号: 标准通道-{deviceId}-{channelNum} => {deviceId}-{channelNum}
const targetValue = edge.target.replace('标准通道-', '').replace('-', '_')
map[sourceKey] = targetValue
return map
},
{} as Record<string, string>
)
generateChannelMapping()
setTimeout(() => {
testPopup.value?.open(
dialogTitle.value,
channelMapping.value,
planId.value,
jwtUtil.getLoginName(),
devIds.value,
standardDevIds.value,
connections
)
}, 100)
}
// 转换 edges.value 为 channelMapping 格式
const channelMapping = ref<Record<string, Record<string, string>>>({})
// 生成映射关系的方法
const generateChannelMapping = () => {
const mapping: Record<string, Record<string, string>> = {}
edges.value.forEach(edge => {
// 解析 source 节点信息(被检通道)
const sourceParts = edge.source.split('-')
const sourceDeviceId = sourceParts[1]
const sourceChannel = sourceParts[2]
// 解析 target 节点信息(标准通道)
const targetParts = edge.target.split('-')
const targetDeviceId = targetParts[1]
const targetChannel = targetParts[2]
// 查找对应的节点以获取显示名称
const sourceDeviceNode = nodes.value.find(node => node.id === sourceDeviceId)
const targetDeviceNode = nodes.value.find(node => node.id === targetDeviceId)
if (sourceDeviceNode && targetDeviceNode) {
// 提取设备显示文本
const sourceDeviceText = sourceDeviceNode.data.label.children[1].children[0].children
const targetDeviceText = targetDeviceNode.data.label.children[1].children[0].children
// 构造键名 - 现在以标准设备为键
const targetKey = `${targetDeviceText}`.replace('设备名称:', '')
const sourceValue = `${sourceDeviceText}通道${sourceChannel}`.replace('设备名称:', '')
// 初始化对象
if (!mapping[targetKey]) {
mapping[targetKey] = {}
}
// 添加映射关系 - 标准设备通道 -> 被检设备信息
mapping[targetKey][`通道${targetChannel}`] = sourceValue
}
})
channelMapping.value = mapping
}
const createNodes = (device: Device.ResPqDev[], standardDev: StandardDevice.ResPqStandardDevice[], deviceMonitor: Map<string, any[]>) => {
const channelCounts: Record<string, number> = {}
device.forEach(device => {
channelCounts[device.id] = device.devChns || 0
})
const inspectionDevices = device.map(d => ({
id: d.id,
name: d.name,
type: 'normal',
deviceType: d.devType
}))
const channelCounts2: Record<string, number> = {}
standardDev.forEach(dev => {
const channelList = dev.inspectChannel ? dev.inspectChannel.split(',') : []
channelCounts2[dev.id] = channelList.length
})
const standardDevices = standardDev.map(d => ({
id: d.id,
name: d.name,
type: 'normal',
deviceType: d.devType
}))
const newNodes: any[] = []
const deviceChannelGroups: { deviceId: string; centerY: number }[] = []
const standardChannelGroups: any[] = []
const deviceWidth = 50
const inputChannelX = 350
const outputChannelX = 1050
const standardWidth = 1170
// 添加被检通道
// let currentYPosition = 50; // 初始Y位置
let actualChannelsTotalLength = 0;
for (const [deviceId, count] of Object.entries(channelCounts)) {
// 直接计算当前设备的通道数并累加,无需完整构建数组
actualChannelsTotalLength += deviceMonitor.has(deviceId)
? (deviceMonitor.get(deviceId) || []).length
: count;
}
let currentYPosition = (vueFlowElement.value - 60 * actualChannelsTotalLength) / 2; // 初始Y位置
const deviceSpacing = 30; // 设备间的垂直间距
Object.entries(channelCounts).forEach(([deviceId, count]) => {
// 从deviceMonitor中获取实际通道信息
let actualChannels = []; // 存储实际的通道号
// 如果deviceMonitor中有该设备的数据则使用实际监测点信息
if (deviceMonitor.has(deviceId)) {
const monitorPoints = deviceMonitor.get(deviceId) || [];
// 提取监测点的num值作为通道号
actualChannels = monitorPoints.map(point => point.num);
//console.log('deviceId', deviceId, '实际通道号:', actualChannels, '监测点:', monitorPoints);
} else {
// 如果没有monitor数据默认使用连续的通道号
actualChannels = Array.from({ length: count }, (_, i) => i + 1);
}
const yPosition = currentYPosition;
// 遍历实际通道号而不是连续的数字
actualChannels.forEach((channelNum, index) => {
const channelId = `被检通道-${deviceId}-${channelNum}`;
newNodes.push({
id: channelId,
type: 'input',
data: { label: createLabel3(`被检通道${channelNum}`) },
position: { x: inputChannelX, y: yPosition + index * 50 },
sourcePosition: 'right',
style: { width: '120px', border: 'none', boxShadow: 'none' }
});
deviceChannelGroups.push({
deviceId,
centerY: 0
});
});
// 更新currentYPosition为下一台设备留出空间
// 每台设备需要的空间 = 实际通道数 * 50 + 设备间距
currentYPosition += actualChannels.length * 50 + deviceSpacing;
});
// 添加标准通道
// let currentYPosition2 = 50; // 初始Y位置
let totalCount = 0;
// 遍历所有条目并累加 count 值
Object.entries(channelCounts2).forEach(([deviceId, count]) => {
totalCount += count;
});
let currentYPosition2 = (vueFlowElement.value - 60 * totalCount) / 2; // 初始Y位置; // 初始Y位置
const standardDeviceSpacing = 30; // 标准设备间的垂直间距
Object.entries(channelCounts2).forEach(([deviceId, count]) => {
const yPosition2 = currentYPosition2;
for (let i = 1; i <= count; i++) {
const channelId = `标准通道-${deviceId}-${i}`;
newNodes.push({
id: channelId,
type: 'output',
data: { label: createLabel3(`标准通道${i}`) },
position: { x: outputChannelX, y: yPosition2 + (i - 1) * 50 },
targetPosition: 'left',
style: { width: '120px', border: 'none', boxShadow: 'none' }
});
standardChannelGroups.push({
deviceId,
centerY: 0
});
}
// 更新currentYPosition2为下一台标准设备留出空间
currentYPosition2 += count * 50 + standardDeviceSpacing;
});
// 添加被检设备
// let deviceCurrentYPosition = 50; // 与通道计算保持一致的初始位置
let deviceCurrentYPosition = (vueFlowElement.value - 60 * actualChannelsTotalLength) / 2; // 与通道计算保持一致的初始位置
let lastDeviceId = ''; // 记录上一个处理的设备ID
deviceChannelGroups.forEach(({ deviceId, centerY }) => {
const device = inspectionDevices.find(d => d.id === deviceId)
if (device) {
// 只有当设备ID变化时才计算新位置
if (lastDeviceId !== deviceId) {
// 计算该设备对应的实际通道数量
let actualChannelCount = channelCounts[deviceId] || 0;
// 如果deviceMonitor中有该设备的数据则使用实际监测点数量
if (deviceMonitor.has(deviceId)) {
const monitorPoints = deviceMonitor.get(deviceId) || [];
actualChannelCount = monitorPoints.length;
}
// 计算设备高度居中位置 - 基于该设备组的实际位置
const deviceCenterY = deviceCurrentYPosition + (actualChannelCount * 50) / 2 - 50
newNodes.push({
id: device.id,
data: { label: createLabel(device.name, device.deviceType, 1) },
position: { x: deviceWidth, y: deviceCenterY },
class: 'no-handle-node',
style: { width: '300px', border: 'none', boxShadow: 'none' }
})
// 更新位置为下一台设备的起始位置
deviceCurrentYPosition += actualChannelCount * 50 + 30
// 更新上一个设备ID
lastDeviceId = deviceId
}
}
})
// 添加标准设备
// let standardDeviceCurrentYPosition = 50; // 与标准通道计算保持一致的初始位置
let standardDeviceCurrentYPosition = (vueFlowElement.value - 60 * totalCount) / 2; // 与标准通道计算保持一致的初始位置
let lastStandardDeviceId = ''; // 记录上一个处理的标准设备ID
standardChannelGroups.forEach(({ deviceId, centerY }) => {
const device = standardDevices.find(d => d.id === deviceId)
if (device) {
// 只有当设备ID变化时才计算新位置
if (lastStandardDeviceId !== deviceId) {
// 计算该标准设备对应的通道数量
const channelCount = channelCounts2[deviceId] || 0
// 计算设备高度居中位置 - 基于该设备组的实际位置
const deviceCenterY = standardDeviceCurrentYPosition + (channelCount * 50) / 2 - 50
newNodes.push({
id: device.id,
data: { label: createLabel(device.name, device.deviceType, 2) },
position: { x: standardWidth, y: deviceCenterY },
class: 'no-handle-node',
style: { width: '300px', border: 'none', boxShadow: 'none' }
})
// 更新位置为下一台标准设备的起始位置
standardDeviceCurrentYPosition += channelCount * 50 + 30
// 更新上一个标准设备ID
lastStandardDeviceId = deviceId
}
}
})
//页面高度取决于设备通道
// dialogHeight.value = Math.max(yPosition.value, yPosition2.value)
return newNodes
}
defineExpose({ open, handleNext })
</script>
<style>
.flow-container {
width: 100%;
overflow: hidden;
}
.vue-flow__node.no-handle-node .vue-flow__handle {
display: none !important;
}
</style>