Zimo преди 1 ден
родител
ревизия
ceb1f1d8b1

+ 772 - 0
src/views/test/air.vue

@@ -0,0 +1,772 @@
+<script setup lang="ts">
+import { ref, onUnmounted } from 'vue'
+import { Graph } from '@antv/x6'
+
+const visible = ref(false)
+
+const psacontainerRef = ref<HTMLDivElement>()
+const graph = ref<Graph | null>(null)
+
+const hoveredNodeId = ref<string | null>(null)
+
+const nodeTooltip = reactive({
+  visible: false,
+  x: 0,
+  y: 0
+})
+
+const initGraph = () => {
+  if (!psacontainerRef.value) return
+
+  graph.value = new Graph({
+    container: psacontainerRef.value,
+    interacting: false,
+    autoResize: true,
+    panning: true,
+    mousewheel: { enabled: true, zoomAtMousePosition: true, modifiers: ['ctrl', 'meta'] },
+    background: { color: 'transparent' },
+    grid: { size: 2, visible: true, type: 'dot', args: { color: '#1E2E4A', thickness: 1 } }
+  })
+
+  graph.value.on('node:mouseenter', ({ node, e }) => {
+    hoveredNodeId.value = node.id
+    nodeTooltip.x = e.clientX + 15
+    nodeTooltip.y = e.clientY + 15
+    nodeTooltip.visible = true
+  })
+  graph.value.on('node:mouseleave', () => {
+    hoveredNodeId.value = null
+    nodeTooltip.visible = false
+  })
+}
+
+const nodesData = reactive<any>([
+  {
+    id: 'fan',
+    x: 140,
+    y: 100,
+    width: 140,
+    height: 140,
+    img: 'fan.png',
+    name: '冷却风机',
+    status: 'running',
+    params: [
+      { label: '风机转速', value: 1450, unit: 'rpm' },
+      { label: '运行电流', value: 45.2, unit: 'A' }
+    ]
+  },
+  {
+    id: 'filter1',
+    x: 460,
+    y: 114,
+    width: 80,
+    height: 240,
+    img: 'filter.png',
+    name: '液气分离',
+    status: 'running',
+    params: [
+      { label: '运行压差', value: 2.1, unit: 'kPa' },
+      { label: '自动排污', value: '开启', unit: '' }
+    ]
+  },
+  {
+    id: 'filter2',
+    x: 680,
+    y: 114,
+    width: 80,
+    height: 240,
+    img: 'filter.png',
+    name: '一级过滤',
+    status: 'running',
+    params: [{ label: '滤芯压差', value: 5.4, unit: 'kPa' }]
+  },
+  {
+    id: 'filter3',
+    x: 900,
+    y: 114,
+    width: 80,
+    height: 240,
+    img: 'filter.png',
+    name: '二级过滤',
+    status: 'running',
+    params: [{ label: '滤芯压差', value: 3.2, unit: 'kPa' }]
+  },
+  {
+    id: 'cool',
+    x: 1160,
+    y: 100,
+    width: 360,
+    height: 240,
+    img: 'cool.png',
+    name: '预冷机',
+    status: 'running',
+    params: [
+      { label: '进口温度', value: 38.5, unit: '°C' },
+      { label: '出口温度', value: 12.0, unit: '°C' },
+      { label: '冷媒压力', value: 0.85, unit: 'MPa' }
+    ]
+  },
+
+  // 阀门不需要显示 Tooltip,因此直接不定义 params 和 name
+  { id: 'vavle1', x: 476, y: 415, width: 50, height: 60, img: 'vavle1.png', angle: 90 },
+  { id: 'vavle2', x: 696, y: 415, width: 50, height: 60, img: 'vavle1.png', angle: 90 },
+  { id: 'vavle3', x: 916, y: 415, width: 50, height: 60, img: 'vavle1.png', angle: 90 },
+  { id: 'vavle4', x: 1040, y: 240, width: 50, height: 60, img: 'vavle1.png', angle: 90 },
+  { id: 'vavle5', x: 1160, y: 355, width: 50, height: 60, img: 'vavle1.png' },
+
+  { id: 'vavlea', x: 375, y: 420, width: 60, height: 50, img: 'vavle2.png', angle: 270 },
+  { id: 'vavleb', x: 595, y: 420, width: 60, height: 50, img: 'vavle2.png', angle: 270 },
+  { id: 'vavlec', x: 815, y: 420, width: 60, height: 50, img: 'vavle2.png', angle: 270 },
+
+  {
+    id: 'filter4',
+    x: 1145,
+    y: 446,
+    width: 80,
+    height: 240,
+    img: 'filter.png',
+    name: '三级过滤',
+    status: 'running',
+    params: [{ label: '滤芯压差', value: 1.5, unit: 'kPa' }]
+  },
+  {
+    id: 'filter5',
+    x: 1320,
+    y: 446,
+    width: 80,
+    height: 240,
+    img: 'filter.png',
+    name: '四级过滤',
+    status: 'running',
+    params: [{ label: '滤芯压差', value: 0.8, unit: 'kPa' }]
+  },
+
+  // 阀门
+  { id: 'vavle6', x: 1161, y: 750, width: 50, height: 60, img: 'vavle1.png', angle: 90 },
+  { id: 'vavle7', x: 1336, y: 750, width: 50, height: 60, img: 'vavle1.png', angle: 90 },
+  { id: 'vavled', x: 1060, y: 755, width: 60, height: 50, img: 'vavle2.png', angle: 270 },
+  { id: 'vavlee', x: 1235, y: 755, width: 60, height: 50, img: 'vavle2.png', angle: 270 },
+
+  // 仪表1:空气流量
+  {
+    id: 'meter1',
+    x: 1475,
+    y: 446,
+    width: 80,
+    height: 70,
+    img: 'meter1.png',
+    name: '流量计',
+    status: 'running',
+    params: [
+      { label: '瞬时流量', value: 1250.5, unit: 'Nm³/h' },
+      { label: '累计流量', value: 458920, unit: 'Nm³' }
+    ]
+  },
+  // 仪表2:空气压力与露点
+  {
+    id: 'meter2',
+    x: 1635,
+    y: 442,
+    width: 80,
+    height: 70,
+    img: 'meter2.png',
+    name: '压力变送器',
+    status: 'running',
+    params: [
+      { label: '空气压力', value: 0.75, unit: 'MPa' },
+      { label: '露点温度', value: -40, unit: '°C' }
+    ]
+  }
+])
+
+// 记录定时器,方便组件销毁时清理
+let simTimer: ReturnType<typeof setInterval> | null = null
+
+// 模拟实时数据波动
+const startSimulation = () => {
+  // 每 2 秒刷新一次数据
+  simTimer = setInterval(() => {
+    nodesData.forEach((dev: any) => {
+      // 只有设备在运行且有参数才更新
+      if (dev.status === 'running' && dev.params && dev.params.length > 0) {
+        dev.params.forEach((param: any) => {
+          // 如果是自动排污的状态文字,跳过计算
+          if (typeof param.value === 'string') return
+
+          // 处理累计流量(只增不减)
+          if (param.label === '累计流量') {
+            // 每次增加 0.1 到 0.5 之间的随机数
+            param.value = Number((param.value + (Math.random() * 0.4 + 0.1)).toFixed(1))
+            return
+          }
+
+          // 处理其他浮动参数
+          // 为了防止数据漫无目的地偏移,我们需要给它一个基准值的概念
+          // 如果没有记录基准值,我们用第一次的值作为基准
+          if (param.baseValue === undefined) {
+            param.baseValue = param.value
+          }
+
+          // 波动幅度设定为基准值的 ±2%
+          const fluctuation = param.baseValue * 0.02
+          const change = (Math.random() * 2 - 1) * fluctuation
+          let newValue = param.baseValue + change
+
+          // 根据不同单位保留合适的小数位数
+          if (param.unit === 'rpm') {
+            param.value = Math.round(newValue) // 转速取整
+          } else if (['A', '°C', 'Nm³/h'].includes(param.unit)) {
+            param.value = Number(newValue.toFixed(1)) // 保留1位小数
+          } else if (['kPa', 'MPa'].includes(param.unit)) {
+            param.value = Number(newValue.toFixed(2)) // 压力保留2位小数
+          } else {
+            param.value = Number(newValue.toFixed(2))
+          }
+        })
+      }
+    })
+  }, 2000) // 2000毫秒(2秒)更新一次
+}
+
+const renderNodes = () => {
+  if (!graph.value) return
+  nodesData.forEach((node) => {
+    graph.value!.addNode({
+      id: node.id,
+      x: node.x,
+      y: node.y,
+      width: node.width,
+      height: node.height,
+      shape: 'image',
+      imageUrl: `src/views/test/image/${node.img}`,
+      angle: node.angle,
+      attrs: {
+        image: {
+          preserveAspectRatio: 'none'
+        },
+        ...(node.name
+          ? {
+              label: {
+                text: node.name,
+                fill: '#00E5FF',
+                fontSize: 14,
+                fontWeight: 'bold',
+                letterSpacing: 2,
+                refX: 0.5,
+                refY: -20,
+                textAnchor: 'middle',
+                textVerticalAnchor: 'middle'
+              }
+            }
+          : {})
+      }
+    })
+  })
+}
+
+const edgeAttrs = {
+  markup: [
+    { tagName: 'path', selector: 'fill' },
+    { tagName: 'path', selector: 'line' }
+  ],
+  attrs: {
+    fill: {
+      connection: true,
+      strokeWidth: 10,
+      stroke: '#0F5BB5',
+      strokeLinecap: 'round',
+      fill: 'none'
+    },
+    line: {
+      connection: true,
+      strokeWidth: 4,
+      stroke: '#fff',
+      strokeDasharray: '20, 30',
+      strokeLinecap: 'round',
+      targetMarker: null,
+      fill: 'none',
+      filter: 'drop-shadow(0 0 3px #fff)',
+      style: {
+        animation: 'edge-dash-flow 1.2s infinite linear'
+      }
+    }
+  }
+}
+
+const edgeAttrs2 = {
+  markup: [
+    { tagName: 'path', selector: 'fill' },
+    { tagName: 'path', selector: 'line' }
+  ],
+  attrs: {
+    fill: {
+      connection: true,
+      strokeWidth: 10,
+      stroke: '#2bbbc6',
+      strokeLinecap: 'round',
+      fill: 'none'
+    },
+    line: {
+      connection: true,
+      strokeWidth: 4,
+      stroke: '#fff',
+      strokeDasharray: '20, 30',
+      strokeLinecap: 'round',
+      targetMarker: null,
+      fill: 'none',
+      filter: 'drop-shadow(0 0 3px #fff)',
+      style: {
+        animation: 'edge-dash-flow 1.2s infinite linear'
+      }
+    }
+  }
+}
+
+const edgeDatas = reactive<any>([
+  {
+    id: 'ac-edge',
+    source: { x: 590, y: 780 },
+    vertices: [
+      { x: 60, y: 780 },
+      { x: 60, y: 165 }
+    ],
+    target: { x: 140, y: 165 }
+  },
+  { id: 'fan-edge', source: { x: 280, y: 165 }, target: { x: 465, y: 165 } },
+  { id: 'filter1-edge', source: { x: 535, y: 165 }, target: { x: 685, y: 165 } },
+  { id: 'filter2-edge', source: { x: 755, y: 165 }, target: { x: 905, y: 165 } },
+  { id: 'vavle4-edge-input', source: { x: 1065, y: 165 }, target: { x: 1065, y: 245 } },
+  { id: 'filter3-edge', source: { x: 975, y: 165 }, target: { x: 1165, y: 165 } },
+  { id: 'vavle5-edge-input', source: { x: 1065, y: 385 }, target: { x: 1162, y: 385 } },
+  {
+    id: 'cool-edge-out',
+    type: 2,
+    source: { x: 1512, y: 278 },
+    vertices: [{ x: 1750, y: 278 }],
+    target: { x: 1750, y: 910 }
+  },
+  {
+    id: 'vavle5-edge-out',
+    source: { x: 1206, y: 385 },
+    vertices: [
+      { x: 1634, y: 385 },
+      { x: 1634, y: 165 }
+    ],
+    target: { x: 1515, y: 165 }
+  },
+
+  {
+    id: 'vavle4-edge-out',
+    source: { x: 1065, y: 290 },
+    vertices: [{ x: 1065, y: 498 }],
+    target: { x: 1145, y: 498 }
+  },
+  { id: 'filter4-edge', source: { x: 1220, y: 498 }, target: { x: 1322, y: 498 } },
+  { id: 'filter5-edge', source: { x: 1396, y: 498 }, target: { x: 1480, y: 498 } },
+  { id: 'meter1-edge', source: { x: 1546, y: 498 }, target: { x: 1646, y: 498 } },
+
+  {
+    id: 'vavlea-edge-input',
+    type: 2,
+    source: { x: 501, y: 350 },
+    vertices: [{ x: 420, y: 350 }],
+    target: { x: 420, y: 424 }
+  },
+  { id: 'vavle1-edge-input', type: 2, source: { x: 501, y: 314 }, target: { x: 501, y: 420 } },
+  { id: 'vavle1-edge-out', type: 2, source: { x: 501, y: 466 }, target: { x: 501, y: 544 } },
+  { id: 'vavlea-edge-out', type: 2, source: { x: 420, y: 466 }, target: { x: 420, y: 544 } },
+  {
+    id: 'vavleb-edge-input',
+    type: 2,
+    source: { x: 721, y: 350 },
+    vertices: [{ x: 640, y: 350 }],
+    target: { x: 640, y: 424 }
+  },
+  { id: 'vavle2-edge-input', type: 2, source: { x: 721, y: 314 }, target: { x: 721, y: 420 } },
+  { id: 'vavle2-edge-out', type: 2, source: { x: 721, y: 466 }, target: { x: 721, y: 544 } },
+  { id: 'vavleb-edge-out', type: 2, source: { x: 640, y: 466 }, target: { x: 640, y: 544 } },
+
+  {
+    id: 'vavlec-edge-input',
+    type: 2,
+    source: { x: 941, y: 350 },
+    vertices: [{ x: 860, y: 350 }],
+    target: { x: 860, y: 424 }
+  },
+  { id: 'vavle3-edge-input', type: 2, source: { x: 941, y: 314 }, target: { x: 941, y: 420 } },
+  { id: 'vavle3-edge-out', type: 2, source: { x: 941, y: 466 }, target: { x: 941, y: 544 } },
+  { id: 'vavlec-edge-out', type: 2, source: { x: 860, y: 466 }, target: { x: 860, y: 544 } },
+
+  {
+    id: 'vavled-edge-input',
+    type: 2,
+    source: { x: 1186, y: 685 },
+    vertices: [{ x: 1105, y: 685 }],
+    target: { x: 1105, y: 756 }
+  },
+  { id: 'vavle6-edge-input', type: 2, source: { x: 1186, y: 648 }, target: { x: 1186, y: 756 } },
+  { id: 'vavle6-edge-out', type: 2, source: { x: 1186, y: 800 }, target: { x: 1186, y: 910 } },
+  { id: 'vavled-edge-out', type: 2, source: { x: 1105, y: 800 }, target: { x: 1105, y: 910 } },
+
+  {
+    id: 'vavlee-edge-input',
+    type: 2,
+    source: { x: 1360, y: 685 },
+    vertices: [{ x: 1279, y: 685 }],
+    target: { x: 1280, y: 756 }
+  },
+  { id: 'vavle7-edge-input', type: 2, source: { x: 1361, y: 648 }, target: { x: 1361, y: 756 } },
+  { id: 'vavle7-edge-out', type: 2, source: { x: 1361, y: 800 }, target: { x: 1361, y: 910 } },
+  { id: 'vavlee-edge-out', type: 2, source: { x: 1280, y: 800 }, target: { x: 1280, y: 910 } },
+  {
+    id: 'nit-edge-input1',
+    source: { x: 1790, y: 498 },
+    vertices: [{ x: 1790, y: 298 }],
+    target: { x: 1960, y: 298 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.78, offset: 25 }
+      }
+    ]
+  },
+  {
+    id: 'nit-edge-input2',
+    source: { x: 1790, y: 498 },
+    vertices: [{ x: 1790, y: 398 }],
+    target: { x: 1960, y: 398 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.7, offset: 25 }
+      }
+    ]
+  },
+  {
+    id: 'nit-edge-input3',
+    source: { x: 1790, y: 498 },
+    vertices: [{ x: 1790, y: 598 }],
+    target: { x: 1960, y: 598 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.7, offset: 25 }
+      }
+    ]
+  },
+  {
+    id: 'nit-edge-input4',
+    source: { x: 1790, y: 498 },
+    vertices: [{ x: 1790, y: 698 }],
+    target: { x: 1960, y: 698 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.78, offset: 25 }
+      }
+    ]
+  },
+  {
+    id: 'meter2-edge-out',
+    source: { x: 1710, y: 498 },
+    target: { x: 1960, y: 498 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: 'PSA入口', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.68, offset: 25 }
+      }
+    ]
+  },
+  {
+    id: 'out',
+    type: 2,
+    source: { x: 420, y: 544 },
+    vertices: [
+      { x: 1000, y: 544 },
+      { x: 1000, y: 910 }
+    ],
+    target: { x: 1960, y: 910 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: '排污收集箱', fill: '#2bbbc6', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.95, offset: -25 }
+      }
+    ]
+  },
+  {
+    id: 'ac-edge-input1',
+    source: { x: 100, y: 1010 },
+    target: { x: 100, y: 790 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: '空\n压\n机\n#1', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.5, offset: -25 }
+      }
+    ]
+  },
+  {
+    id: 'ac-edge-input2',
+    source: { x: 200, y: 1010 },
+    target: { x: 200, y: 790 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: '空\n压\n机\n2#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.5, offset: -25 }
+      }
+    ]
+  },
+  {
+    id: 'ac-edge-input3',
+    source: { x: 300, y: 1010 },
+    target: { x: 300, y: 790 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: '空\n压\n机\n3#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.5, offset: -25 }
+      }
+    ]
+  },
+  {
+    id: 'ac-edge-input4',
+    source: { x: 400, y: 1010 },
+    target: { x: 400, y: 790 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: '空\n压\n机\n4#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.5, offset: -25 }
+      }
+    ]
+  },
+  {
+    id: 'ac-edge-input5',
+    source: { x: 500, y: 1010 },
+    target: { x: 500, y: 790 },
+    targetMarker: { name: 'block', width: 18, height: 24, fill: '#fff', stroke: 'none' },
+    labels: [
+      {
+        attrs: {
+          text: { text: '空\n压\n机\n5#', fill: '#00E5FF', fontSize: 20, fontWeight: 'bold' },
+          rect: { fill: 'transparent' }
+        },
+        position: { distance: 0.5, offset: -25 }
+      }
+    ]
+  }
+])
+
+const renderEdges = () => {
+  edgeDatas.forEach((edgeData) => {
+    const { type = 1, targetMarker, ...other } = edgeData,
+      attrs = type === 1 ? edgeAttrs : edgeAttrs2
+    const edge = graph.value?.addEdge({ ...other, ...attrs })
+    if (targetMarker) {
+      edge?.attr('line/targetMarker', targetMarker)
+    }
+  })
+}
+
+function handleOpen() {
+  visible.value = true
+
+  nextTick(() => {
+    initGraph()
+    renderEdges()
+    renderNodes()
+
+    startSimulation()
+  })
+}
+
+function dispose() {
+  graph.value?.dispose()
+
+  if (simTimer) {
+    clearInterval(simTimer)
+    simTimer = null
+  }
+}
+
+defineExpose({
+  handleOpen
+})
+
+onUnmounted(() => {
+  dispose()
+})
+
+function handleClose() {
+  visible.value = false
+  dispose()
+}
+</script>
+
+<template>
+  <el-dialog
+    width="90%"
+    top="5vh"
+    class="h-90vh psa-dialog"
+    v-model="visible"
+    :show-close="false"
+    @closed="dispose"
+  >
+    <div class="relative size-full overflow-hidden bg-[#0B1120] font-sans rounded-lg flex flex-col">
+      <header
+        class="relative z-20 flex items-center justify-center h-16 shrink-0 bg-[#0B1120] border-b border-[#00E5FF]/40 shadow-[0_4px_20px_rgba(0,229,255,0.15)]"
+      >
+        <div
+          class="absolute left-10 right-10 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-80"
+        ></div>
+        <h1
+          class="text-2xl font-bold tracking-[0.2em] text-[#00E5FF] drop-shadow-[0_0_10px_rgba(0,229,255,0.8)]"
+        >
+          空气处理橇组态图
+        </h1>
+
+        <button
+          @click="handleClose"
+          class="absolute bg-transparent border-none right-6 top-1/2 -translate-y-1/2 flex items-center justify-center w-10 h-10 text-[#00E5FF]/60 hover:text-[#00E5FF] transition-all duration-300 group focus:outline-none"
+          title="关闭"
+        >
+          <div
+            class="i-ep-close w-6 h-6 relative z-10 group-hover:drop-shadow-[0_0_8px_rgba(0,229,255,0.9)] transition-all duration-300 group-hover:rotate-90"
+          ></div>
+        </button>
+      </header>
+      <main class="flex-1 relative w-full h-full z-0" ref="psacontainerRef"></main>
+      <template v-if="hoveredNodeId">
+        <template v-for="dev in nodesData" :key="dev.id">
+          <div
+            v-if="dev.id === hoveredNodeId && nodeTooltip.visible && dev.params"
+            class="coord-tooltip"
+            :style="{ left: `${nodeTooltip.x}px`, top: `${nodeTooltip.y}px`, minWidth: '200px' }"
+          >
+            <div
+              style="
+                padding-bottom: 6px;
+                margin-bottom: 4px;
+                font-size: 14px;
+                font-weight: bold;
+                color: #fff;
+                border-bottom: 1px dashed rgb(14 165 233 / 40%);
+              "
+            >
+              {{ dev.name }}
+            </div>
+            <div class="coord-item">
+              <span>运行状态:</span>
+              <span
+                class="val"
+                :style="{ color: dev.status === 'running' ? '#00FF7F' : '#ef4444' }"
+              >
+                {{ dev.status === 'running' ? '运行中' : '已停机' }}
+              </span>
+            </div>
+            <div class="coord-item" v-for="(param, index) in dev.params" :key="index">
+              <span>{{ param.label }}:</span>
+              <span class="val transition-all">{{ param.value }} {{ param.unit }}</span>
+            </div>
+          </div>
+        </template>
+      </template>
+    </div>
+  </el-dialog>
+</template>
+
+<style>
+.psa-dialog {
+  padding: 0;
+  background-color: transparent;
+}
+
+.psa-dialog .el-dialog__header {
+  display: none;
+}
+
+.psa-dialog .el-dialog__body {
+  width: 100%;
+  height: 100%;
+  padding: 0; /* 移除默认 padding,让背景铺满 */
+}
+
+@keyframes edge-dash-flow {
+  0% {
+    stroke-dashoffset: 50;
+  }
+
+  100% {
+    stroke-dashoffset: 0;
+  }
+}
+
+.coord-tooltip {
+  position: fixed;
+  z-index: 9999;
+  display: flex;
+  padding: 12px 16px;
+  font-family: monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #fff;
+  white-space: nowrap;
+  pointer-events: none;
+  background: rgb(10 22 40 / 95%);
+  border: 1px solid rgb(0 229 255 / 80%);
+  border-radius: 6px;
+  box-shadow:
+    0 4px 15px rgb(0 0 0 / 80%),
+    0 0 10px rgb(0 229 255 / 30%) inset;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.coord-item {
+  display: flex;
+  justify-content: space-between;
+  gap: 24px;
+}
+
+.coord-item .val {
+  font-weight: bold;
+  color: #00e5ff;
+}
+</style>

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/ac.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/air.svg


BIN
src/views/test/image/cool.png


BIN
src/views/test/image/fan.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/fillwater.svg


BIN
src/views/test/image/filter.png


BIN
src/views/test/image/meter1.png


BIN
src/views/test/image/meter2.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/nit.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/oli.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/supercharge.svg


BIN
src/views/test/image/vavle1.png


BIN
src/views/test/image/vavle2.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/views/test/image/water.svg


+ 1010 - 205
src/views/test/index.vue

@@ -1,244 +1,1049 @@
 <script lang="ts" setup>
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
 import { Graph } from '@antv/x6'
+import air from './air.vue'
 
 const containerRef = ref<HTMLDivElement>()
-let graph: Graph | null = null
+const graph = ref<Graph | null>(null)
+let timer: number | null = null
 
-// 1. 注册自定义节点
-Graph.registerNode(
-  'custom-node',
+// === 看板模拟数据 ===
+const overviewData = reactive({
+  totalFlow: 12450,
+  pressure: 24.5,
+  temperature: 35.2,
+  efficiency: 92.4
+})
+
+const wellData = reactive({
+  wellheadPressure: 32.1,
+  dailyInjection: 4500,
+  cumulative: 1250
+})
+
+const alarmLogs = reactive([
+  { time: '14:20:15', msg: '#4空压机例行停机维护', level: 'info' },
+  { time: '13:45:00', msg: '增压橇出口压力小幅波动', level: 'warn' },
+  { time: '11:10:22', msg: '系统能效自动优化完成', level: 'info' },
+  { time: '09:05:11', msg: '2号通讯模块重连成功', level: 'info' }
+])
+
+const gasComposition = reactive([
+  { name: 'N2 (氮气)', value: 95.0, color: 'bg-blue-500' },
+  { name: 'O2 (氧气)', value: 3.0, color: 'bg-cyan-500' },
+  { name: 'CO2 (二氧化碳)', value: 1.5, color: 'bg-teal-500' },
+  { name: '其他', value: 0.5, color: 'bg-slate-500' }
+])
+
+// === 核心改造:所有设备统一数据源 (驱动面板、X6节点、Tooltip) ===
+// 我们额外存储一个 defaultParams 用于设备恢复启动时参考
+const deviceData = reactive([
+  {
+    id: 'ac5',
+    name: '#5 空压机',
+    status: 'running',
+    x: 250,
+    y: 130,
+    width: 400,
+    height: 220,
+    img: 'ac.svg',
+    refX: 0.78,
+    refY: 0,
+    params: [
+      { label: '负载率', value: 90, unit: '%' },
+      { label: '排温', value: 70, unit: '°C' }
+    ],
+    defaultParams: [90, 70]
+  },
+  {
+    id: 'ac4',
+    name: '#4 空压机',
+    status: 'stopped',
+    x: 200,
+    y: 200,
+    width: 400,
+    height: 220,
+    img: 'ac.svg',
+    refX: 0.78,
+    refY: 0,
+    params: [
+      { label: '负载率', value: 0, unit: '%' },
+      { label: '排温', value: 25, unit: '°C' }
+    ],
+    defaultParams: [85, 65]
+  },
+  {
+    id: 'ac3',
+    name: '#3 空压机',
+    status: 'running',
+    x: 150,
+    y: 270,
+    width: 400,
+    height: 220,
+    img: 'ac.svg',
+    refX: 0.78,
+    refY: 0,
+    params: [
+      { label: '负载率', value: 88, unit: '%' },
+      { label: '排温', value: 68, unit: '°C' }
+    ],
+    defaultParams: [88, 68]
+  },
+  {
+    id: 'ac2',
+    name: '#2 空压机',
+    status: 'running',
+    x: 100,
+    y: 340,
+    width: 400,
+    height: 220,
+    img: 'ac.svg',
+    refX: 0.78,
+    refY: 0,
+    params: [
+      { label: '负载率', value: 82, unit: '%' },
+      { label: '排温', value: 62, unit: '°C' }
+    ],
+    defaultParams: [82, 62]
+  },
+  {
+    id: 'ac1',
+    name: '#1 空压机',
+    status: 'running',
+    x: 50,
+    y: 410,
+    width: 400,
+    height: 220,
+    img: 'ac.svg',
+    refX: 0.78,
+    refY: 0,
+    params: [
+      { label: '负载率', value: 85, unit: '%' },
+      { label: '排温', value: 65, unit: '°C' }
+    ],
+    defaultParams: [85, 65]
+  },
+  {
+    id: 'air',
+    name: '空气处理橇',
+    status: 'running',
+    x: 850,
+    y: 270,
+    width: 400,
+    height: 320,
+    img: 'air.svg',
+    event: 'node:detail',
+    refX: 0.6,
+    refY: 0,
+    params: [
+      { label: '处理量', value: 1200, unit: 'm³/h' },
+      { label: '露点', value: -40, unit: '°C' }
+    ],
+    defaultParams: [1200, -40]
+  },
+  {
+    id: 'nit',
+    name: '制氮橇',
+    status: 'running',
+    x: 1520,
+    y: 310,
+    width: 600,
+    height: 240,
+    img: 'nit.svg',
+    refX: 0.8,
+    refY: 0,
+    params: [
+      { label: '产氮量', value: 1000, unit: 'Nm³/h' },
+      { label: '纯度', value: 95.5, unit: '%' }
+    ],
+    defaultParams: [1000, 95.5]
+  },
+  {
+    id: 'supercharge',
+    name: '增压橇',
+    status: 'running',
+    x: 1420,
+    y: -140,
+    width: 700,
+    height: 300,
+    img: 'supercharge.svg',
+    refX: 0.8,
+    refY: 0.3,
+    params: [
+      { label: '入口压', value: 0.8, unit: 'MPa' },
+      { label: '出口压', value: 25.4, unit: 'MPa' }
+    ],
+    defaultParams: [0.8, 25.4]
+  },
+  {
+    id: 'oli',
+    name: '抽油机',
+    status: 'running',
+    x: 2350,
+    y: -880,
+    width: 1000,
+    height: 800,
+    img: 'oli.svg',
+    refX: 0.8,
+    refY: 0,
+    params: [
+      { label: '冲程', value: 3.5, unit: 'm' },
+      { label: '冲次', value: 6, unit: '次/分' }
+    ],
+    defaultParams: [3.5, 6]
+  },
   {
+    id: 'fillwater1',
+    name: '#1 注水泵',
+    status: 'stopped',
+    x: 2610,
+    y: 250,
+    width: 320,
+    height: 260,
+    img: 'fillwater.svg',
+    refX: 0.8,
+    refY: 0,
+    params: [
+      { label: '泵压', value: 0, unit: 'MPa' },
+      { label: '流量', value: 0, unit: 'm³/h' }
+    ],
+    defaultParams: [15.2, 45]
+  },
+  {
+    id: 'fillwater2',
+    name: '#2 注水泵',
+    status: 'running',
+    x: 3240,
+    y: 250,
+    width: 320,
+    height: 260,
+    img: 'fillwater.svg',
+    refX: 0.8,
+    refY: 0,
+    params: [
+      { label: '泵压', value: 15.2, unit: 'MPa' },
+      { label: '流量', value: 45, unit: 'm³/h' }
+    ],
+    defaultParams: [15.2, 45]
+  },
+  {
+    id: 'water1',
+    name: '#1 水罐',
+    status: 'running',
+    x: 2690,
+    y: 600,
+    width: 260,
+    height: 260,
+    img: 'water.svg',
+    refX: 0.8,
+    refY: 0,
+    params: [{ label: '液位', value: 3.2, unit: 'm' }],
+    defaultParams: [3.2]
+  },
+  {
+    id: 'water2',
+    name: '#2 水罐',
+    status: 'running',
+    x: 3310,
+    y: 600,
+    width: 260,
+    height: 260,
+    img: 'water.svg',
+    refX: 0.8,
+    refY: 0,
+    params: [{ label: '液位', value: 4.5, unit: 'm' }],
+    defaultParams: [4.5]
+  }
+])
+
+const hoveredNodeId = ref<string | null>(null)
+
+const nodeTooltip = reactive({
+  visible: false,
+  x: 0,
+  y: 0
+})
+
+const toggleDevice = (dev: any) => {
+  dev.status = dev.status === 'running' ? 'stopped' : 'running'
+
+  alarmLogs.unshift({
+    time: new Date().toTimeString().split(' ')[0],
+    msg: `手动${dev.status === 'running' ? '开启' : '关闭'} ${dev.name}`,
+    level: dev.status === 'running' ? 'info' : 'warn'
+  })
+  if (alarmLogs.length > 10) alarmLogs.pop()
+
+  if (dev.status === 'running') {
+    dev.params.forEach((p: any, idx: number) => {
+      if (p.value <= 0.1) p.value = dev.defaultParams[idx]
+    })
+  }
+
+  if (graph.value) {
+    const edgeId = `${dev.id}-edge`
+    const edge = graph.value.getCellById(edgeId)
+    if (edge) {
+      if (dev.status === 'running') {
+        edge.attr('line/style/animation', 'edge-dash-flow 1.2s infinite linear')
+        edge.attr('fill/stroke', '#0F5BB5')
+      } else {
+        edge.attr('line/style/animation', 'none')
+        edge.attr('fill/stroke', '#B50F0F')
+      }
+    }
+  }
+}
+
+const updateDeviceBaseStats = () => {
+  deviceData.forEach((dev) => {
+    if (dev.status === 'running') {
+      dev.params.forEach((p) => {
+        // 排除掉由工艺链计算的“处理量”、“压力”等核心参数,只对排温、负载等进行基础波动
+        if (['排温', '负载率', '冲程', '冲次', '液位'].includes(p.label)) {
+          const range = Math.max(0.1, p.value * 0.01)
+          p.value = Number((p.value + (Math.random() - 0.5) * range).toFixed(1))
+        }
+      })
+    } else {
+      // 停机状态下参数缓慢归零/降至环境值
+      dev.params.forEach((p) => {
+        const target = p.label === '排温' ? 25 : 0 // 温度降至25度,其余归0
+        if (p.value > target) {
+          p.value = Number((p.value * 0.8 + target * 0.2).toFixed(1))
+          if (Math.abs(p.value - target) < 0.2) p.value = target
+        }
+      })
+    }
+  })
+}
+
+const startRealtimeSimulation = () => {
+  timer = window.setInterval(() => {
+    // === 第一步:计算空压机源头输出 ===
+    const runningACs = deviceData.filter((d) => d.id.startsWith('ac') && d.status === 'running')
+    const totalACFlow = runningACs.length * 250 // 每台提供250单位流量
+
+    // === 第二步:空气处理橇联动 (依赖空压机) ===
+    const airDevice = deviceData.find((d) => d.id === 'air')!
+    const airInflow = totalACFlow
+    // 处理量:如果自身开启,则等于进气量;如果自身关闭,处理量为0
+    const airOutflow = airDevice.status === 'running' ? airInflow : 0
+    airDevice.params[0].value = airOutflow // 处理量
+
+    // === 第三步:制氮橇联动 (依赖空气处理) ===
+    const nitDevice = deviceData.find((d) => d.id === 'nit')!
+    const nitParam_Flow = nitDevice.params.find((p) => p.label === '产氮量')!
+    const nitParam_Purity = nitDevice.params.find((p) => p.label === '纯度')!
+
+    if (nitDevice.status === 'running' && airOutflow > 100) {
+      // 正常产氮,纯度稳定
+      nitParam_Flow.value = Number((airOutflow * 0.85).toFixed(1))
+      nitParam_Purity.value = Number((95.5 + (Math.random() - 0.5) * 0.2).toFixed(1))
+    } else {
+      // 没气或者关机,产氮量归零,纯度下降
+      nitParam_Flow.value = Number((nitParam_Flow.value * 0.7).toFixed(1))
+      nitParam_Purity.value = Math.max(0, Number((nitParam_Purity.value * 0.9).toFixed(1)))
+    }
+
+    // === 第四步:注水泵系统 (独立分支) ===
+    const waterPumps = deviceData.filter((d) => d.id.startsWith('fillwater'))
+    let totalWaterFlow = 0
+    waterPumps.forEach((pump) => {
+      if (pump.status === 'running') {
+        pump.params[0].value = Number((15.2 + (Math.random() - 0.5)).toFixed(1)) // 泵压
+        pump.params[1].value = Number((45 + (Math.random() - 0.5)).toFixed(1)) // 流量
+        totalWaterFlow += pump.params[1].value
+      } else {
+        pump.params[0].value = Number((pump.params[0].value * 0.5).toFixed(1))
+        pump.params[1].value = 0
+      }
+    })
+
+    // === 第五步:增压橇 (综合气+水压力) ===
+    const superDevice = deviceData.find((d) => d.id === 'supercharge')!
+    const gasIn = nitParam_Flow.value
+    const superInPress = superDevice.params.find((p) => p.label === '入口压')!
+    const superOutPress = superDevice.params.find((p) => p.label === '出口压')!
+
+    // 入口压由气源决定
+    superInPress.value = Number(((gasIn / 1200) * 0.8).toFixed(2))
+    if (superDevice.status === 'running' && gasIn > 10) {
+      // 增压至25MPa左右
+      superOutPress.value = Number((25.4 + (Math.random() - 0.5) * 0.5).toFixed(1))
+    } else {
+      superOutPress.value = Math.max(0, Number((superOutPress.value * 0.8).toFixed(1)))
+    }
+
+    // === 第六步:大屏看板 & 井口数据联动 ===
+    // 1. 场站总览
+    overviewData.totalFlow = Math.floor(gasIn)
+    overviewData.pressure = superOutPress.value
+    overviewData.efficiency = gasIn > 0 ? Number((90 + Math.random() * 5).toFixed(1)) : 0
+    overviewData.temperature = gasIn > 0 ? Number((35 + Math.random() * 5).toFixed(1)) : 25
+
+    // 2. 井口数据
+    wellData.wellheadPressure = Number((overviewData.pressure * 0.98).toFixed(1))
+    if (wellData.wellheadPressure > 2) {
+      // 只有压力够才算注气成功
+      wellData.dailyInjection += Math.floor(gasIn / 3600)
+      wellData.cumulative = Number((wellData.cumulative + 0.0001).toFixed(4))
+    }
+
+    // 3. 气体成分随产量微调 (模拟)
+    if (gasIn > 10) {
+      gasComposition[0].value = Number((95 + (Math.random() - 0.5) * 0.5).toFixed(1))
+      gasComposition[1].value = Number((100 - gasComposition[0].value - 2).toFixed(1))
+    }
+
+    // 处理排温等基础属性波动
+    updateDeviceBaseStats()
+  }, 1000)
+}
+
+const renderGroundAndFence = () => {
+  if (!graph.value) return
+  const baseX = -1190
+  const baseY = -810
+  const wallHeight = 400
+
+  graph.value.addNode({
+    id: 'station-ground',
+    shape: 'polygon',
+    x: baseX,
+    y: baseY,
+    width: 6800,
+    height: 2400,
+    points: '1200,0 4500,0 3600,1800 0,1800',
+    zIndex: -10,
     attrs: {
-      body: { refWidth: 1, refHeight: 1, rx: 4 }
-    },
-    markup: [
-      { tagName: 'rect', selector: 'body' },
-      { tagName: 'path', selector: 'icon' },
-      { tagName: 'text', selector: 'title' }
-    ]
-  },
-  true
-)
-
-// 2. 抽离通用样式配置 (DRY原则,消除重复)
-const commonNodeAttrs = {
-  body: {
+      body: {
+        fill: 'rgba(0, 229, 255, 0.04)',
+        stroke: 'rgba(0, 229, 255, 0.5)',
+        strokeWidth: 2,
+        strokeDasharray: '15, 10',
+        filter: 'drop-shadow(0 0 20px rgba(0,229,255,0.2))'
+      }
+    }
+  })
+
+  const wallAttrs = {
+    body: {
+      fill: 'rgba(0, 229, 255, 0.08)',
+      stroke: 'rgba(0, 229, 255, 0.6)',
+      strokeWidth: 2,
+      strokeDasharray: '5, 5',
+      filter: 'drop-shadow(0 0 10px rgba(0,229,255,0.3))'
+    }
+  }
+  graph.value.addNode({
+    id: 'station-fence-back',
+    shape: 'polygon',
+    x: baseX + 1813.3,
+    y: baseY - wallHeight,
+    width: 4986.7,
+    height: wallHeight,
+    points: `0,${wallHeight} 4986.7,${wallHeight} 4986.7,0 0,0`,
+    zIndex: -9,
+    attrs: wallAttrs
+  })
+  graph.value.addNode({
+    id: 'station-fence-left',
+    shape: 'polygon',
+    x: baseX,
+    y: baseY - wallHeight,
+    width: 1813.3,
+    height: 2800,
+    points: `0,2800 1813.3,${wallHeight} 1813.3,0 0,2400`,
+    zIndex: -8,
+    attrs: { body: { ...wallAttrs.body, fill: 'rgba(0, 229, 255, 0.12)' } }
+  })
+  graph.value.addNode({
+    id: 'station-fence-right',
+    shape: 'polygon',
+    x: baseX + 5440,
+    y: baseY - wallHeight,
+    width: 1360,
+    height: 2800,
+    points: `1360,${wallHeight} 0,2800 0,2400 1360,0`,
+    zIndex: -8,
+    attrs: { body: { ...wallAttrs.body, fill: 'rgba(0, 229, 255, 0.12)' } }
+  })
+  graph.value.addNode({
+    id: 'station-fence-front',
+    shape: 'polygon',
+    x: baseX,
+    y: baseY + 2400 - wallHeight,
+    width: 5440,
+    height: wallHeight,
+    points: `0,${wallHeight} 5440,${wallHeight} 5440,0 0,0`,
+    zIndex: 10,
+    attrs: {
+      body: { ...wallAttrs.body, fill: 'rgba(0, 229, 255, 0.03)', stroke: 'rgba(0, 229, 255, 0.8)' }
+    }
+  })
+}
+
+const edgeAttrs = {
+  markup: [
+    { tagName: 'path', selector: 'fill' },
+    { tagName: 'path', selector: 'line' }
+  ],
+  attrs: {
     fill: {
-      type: 'linearGradient',
-      stops: [
-        { offset: '0%', color: 'rgba(10, 30, 60, 0.9)' },
-        { offset: '100%', color: 'rgba(4, 10, 25, 0.9)' }
-      ]
+      connection: true,
+      strokeWidth: 20,
+      stroke: '#0F5BB5',
+      strokeLinecap: 'round',
+      fill: 'none'
     },
-    stroke: '#00EAFF',
-    strokeWidth: 2,
-    rx: 6,
-    filter: {
-      name: 'dropShadow',
-      args: { dx: 0, dy: 0, blur: 15, color: '#00EAFF' }
+    line: {
+      connection: true,
+      strokeWidth: 10,
+      stroke: '#fff',
+      strokeDasharray: '20, 30',
+      strokeLinecap: 'round',
+      targetMarker: null,
+      fill: 'none',
+      filter: 'drop-shadow(0 0 3px #fff)'
     }
-  },
-  icon: {
-    fill: '#00EAFF',
-    refX: 0.5,
-    refY: 0.4,
-    xAlign: 'middle',
-    yAlign: 'middle',
-    transform: 'scale(3)'
-  },
-  title: {
-    fill: '#FFFFFF',
-    fontSize: 18,
-    fontWeight: 'bold',
-    letterSpacing: 4,
-    refX: 0.5,
-    refY: 0.8,
-    textAnchor: 'middle',
-    textVerticalAnchor: 'middle'
   }
 }
 
-// 3. 集中管理节点数据和 SVG 图标路径
-const ICONS = {
-  compressor:
-    'M10.6 22q-1.275 0-1.937-.763T8 19.5q0-.65.288-1.263t.887-1.012q.55-.35.888-.9t.462-1.175l-.3-.15q-.15-.075-.275-.175l-2.3.825q-.425.15-.825.25T6 16q-1.575 0-2.788-1.375T2 10.6q0-1.275.763-1.937T4.475 8q.65 0 1.275.288t1.025.887q.35.55.9.887t1.175.463l.15-.3q.075-.15.175-.275l-.825-2.3q-.15-.425-.25-.825t-.1-.8q0-1.6 1.375-2.813T13.4 2q1.275 0 1.938.763T16 4.475q0 .65-.288 1.275t-.887 1.025q-.55.35-.887.9t-.463 1.175l.3.15q.15.075.275.175l2.3-.85q.425-.15.813-.237T17.975 8Q20 8 21 9.675t1 3.725q0 1.275-.8 1.938T19.425 16q-.625 0-1.213-.288t-.987-.887q-.35-.55-.9-.887t-1.175-.463l-.15.3q-.075.15-.175.275l.825 2.3q.15.4.25.763t.1.762q.025 1.625-1.35 2.875T10.6 22m1.4-8.5q.625 0 1.062-.437T13.5 12t-.437-1.062T12 10.5t-1.062.438T10.5 12t.438 1.063T12 13.5',
-  airSkid:
-    'M14.5 17c0 1.65-1.35 3-3 3s-3-1.35-3-3h2c0 .55.45 1 1 1s1-.45 1-1s-.45-1-1-1H2v-2h9.5c1.65 0 3 1.35 3 3M19 6.5C19 4.57 17.43 3 15.5 3S12 4.57 12 6.5h2c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5S16.33 8 15.5 8H2v2h13.5c1.93 0 3.5-1.57 3.5-3.5m-.5 4.5H2v2h16.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5v2c1.93 0 3.5-1.57 3.5-3.5S20.43 11 18.5 11',
-  psa: 'M3 21q-.825 0-1.412-.587T1 19v-5q0-.825.588-1.412T3 12V7.725q-.45-.275-.725-.712T2 6V5q0-.825.588-1.412T4 3h5q.825 0 1.413.588T11 5v1q0 .575-.275 1.013T10 7.725V12h4V7.725q-.45-.275-.725-.712T13 6V5q0-.825.588-1.412T15 3h5q.825 0 1.413.588T22 5v1q0 .575-.275 1.013T21 7.725V12q.825 0 1.413.588T23 14v5q0 .825-.587 1.413T21 21zm13-9h3V8h-3zM5 12h3V8H5z',
-  supercharger:
-    'M11.25 16.95V13.8l-2.225 2.225q.5.35 1.063.575t1.162.35m1.5-.025q.6-.1 1.163-.325t1.062-.575L12.75 13.8zm3.275-1.95q.35-.5.563-1.062t.337-1.163H13.8zM13.8 11.25h3.125q-.125-.575-.338-1.137t-.562-1.063zm-1.05-1.05l2.225-2.225q-.5-.35-1.063-.575t-1.162-.35zm-.038 2.513Q13 12.425 13 12t-.288-.712T12 11t-.712.288T11 12t.288.713T12 13t.713-.288M11.25 10.2V7.075q-.6.1-1.162.325t-1.063.575zm-4.175 1.05H10.2l-2.225-2.2q-.35.5-.575 1.05t-.325 1.15m.9 3.725L10.2 12.75H7.05q.125.6.35 1.163t.575 1.062M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21z',
-  waterPump:
-    'm12 9l-.85 1.25q-.675 1.05-.913 1.613T10 13q0 .825.588 1.413T12 15t1.413-.587T14 13q0-.575-.238-1.137t-.912-1.613zm-4.425 7.425Q5.75 14.6 5.75 12t1.825-4.425T12 5.75t4.425 1.825T18.25 12t-1.825 4.425T12 18.25t-4.425-1.825M21 12v-1h-1.325q-.275-2-1.437-3.588T15.325 5H21V4h2v8zM1 20v-8h2v1h1.325q.275 2 1.438 3.588T8.675 19H3v1z',
-  pipe: 'M16 9v2H8V9h2V8H4v2H2V2h2v2h8a2 2 0 0 1 2 2v3zm-6 6v3a2 2 0 0 0 2 2h8v2h2v-8h-2v2h-6v-1h2v-2H8v2z'
-}
-
-const nodesData = [
-  { id: 'ac1', title: '空压机', x: 40, y: 40, icon: ICONS.compressor },
-  { id: 'ac2', title: '空压机', x: 40, y: 280, icon: ICONS.compressor },
-  { id: 'ac3', title: '空压机', x: 40, y: 520, icon: ICONS.compressor },
-  { id: 'Air', title: '空气处理橇', x: 280, y: 280, icon: ICONS.airSkid },
-  { id: 'PSA', title: 'PSA', x: 520, y: 280, icon: ICONS.psa }, // 注意:原代码这里标题写的是空气处理橇,但图标是PSA,请确认是否需改名
-  { id: 'supercharger', title: '增压机', x: 760, y: 280, icon: ICONS.supercharger },
-  { id: 'waterpump1', title: '注水泵', x: 520, y: 680, icon: ICONS.waterPump },
-  { id: 'waterpump2', title: '注水泵', x: 520, y: 920, icon: ICONS.waterPump },
-  { id: 'casing', title: '套管', x: 1220, y: 680, icon: ICONS.pipe },
-  { id: 'olitube', title: '油管', x: 1220, y: 920, icon: ICONS.pipe }
+const edgeDatas = [
+  {
+    id: 'ac1-edge',
+    source: { x: 425, y: 600 },
+    vertices: [{ x: 725, y: 600 }],
+    target: { x: 760, y: 540 }
+  },
+  { id: 'ac2-edge', source: { x: 475, y: 530 }, target: { x: 900, y: 530 } },
+  { id: 'ac3-edge', source: { x: 520, y: 460 }, target: { x: 900, y: 460 } },
+  {
+    id: 'ac5-edge',
+    source: { x: 620, y: 322 },
+    vertices: [{ x: 860, y: 322 }],
+    target: { x: 820, y: 388 }
+  },
+  { id: 'ac4-edge', source: { x: 570, y: 392 }, target: { x: 920, y: 392 } },
+  { id: 'air-edge', source: { x: 1190, y: 480 }, target: { x: 1540, y: 480 } },
+  {
+    id: 'nit-edge',
+    source: { x: 2110, y: 480 },
+    vertices: [
+      { x: 2280, y: 480 },
+      { x: 2245, y: 250 },
+      { x: 1280, y: 250 },
+      { x: 1345, y: 95 }
+    ],
+    target: { x: 1425, y: 95 }
+  },
+  {
+    id: 'water1-edge',
+    source: { x: 2935, y: 800 },
+    vertices: [
+      { x: 3075, y: 800 },
+      { x: 3000, y: 450 }
+    ],
+    target: { x: 2900, y: 450 }
+  },
+  {
+    id: 'water2-edge',
+    source: { x: 3345, y: 800 },
+    vertices: [
+      { x: 3220, y: 800 },
+      { x: 3140, y: 450 }
+    ],
+    target: { x: 3275, y: 450 }
+  },
+  {
+    id: 'fillwater1-edge',
+    source: { x: 2640, y: 450 },
+    vertices: [{ x: 2480, y: 450 }],
+    target: { x: 2420, y: 95 }
+  },
+  {
+    id: 'fillwater2-edge',
+    source: { x: 3535, y: 450 },
+    vertices: [{ x: 3710, y: 450 }],
+    target: { x: 3600, y: 95 }
+  },
+  {
+    id: 'supercharge-edge',
+    source: { x: 2090, y: 95 },
+    vertices: [
+      { x: 3800, y: 95 },
+      { x: 3700, y: -194 }
+    ],
+    target: { x: 3330, y: -191 }
+  }
 ]
 
-const edgesData = [
-  // 三个空压机流向空气处理橇
-  { source: 'ac1', target: 'Air' },
-  { source: 'ac2', target: 'Air' },
-  { source: 'ac3', target: 'Air' },
-
-  // 空气处理橇 -> PSA -> 增压机
-  { source: 'Air', target: 'PSA' },
-  { source: 'PSA', target: 'supercharger' },
+const nodeWithTitleMarkup = [
+  { tagName: 'image', selector: 'image' },
+  { tagName: 'rect', selector: 'titleBg' },
+  { tagName: 'text', selector: 'titleText' }
+]
 
-  { source: 'supercharger', target: 'casing' },
-  { source: 'supercharger', target: 'olitube' },
+const getTechTextAttrs = (
+  title: string,
+  options: { width?: number; refX?: number; refY?: number } = {}
+) => {
+  const { width = 160, refX = 0.8, refY = 0.18 } = options
+  const height = 32
+  return {
+    titleBg: {
+      refX,
+      refY,
+      width,
+      height,
+      x: -(width / 2),
+      y: -(height / 2),
+      fill: 'rgba(11, 17, 32, 0.8)',
+      stroke: '#00E5FF',
+      strokeWidth: 1,
+      rx: 4,
+      ry: 4
+    },
+    titleText: {
+      text: title,
+      fill: '#00E5FF',
+      fontSize: 15,
+      fontWeight: 'bold',
+      letterSpacing: 2,
+      refX,
+      refY,
+      textAnchor: 'middle',
+      textVerticalAnchor: 'middle',
+      filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.8))'
+    }
+  }
+}
 
-  { source: 'waterpump1', target: 'casing' },
-  { source: 'waterpump2', target: 'olitube' }
-]
+const airDialogRef = ref()
 
-onMounted(() => {
+const initGraph = () => {
   if (!containerRef.value) return
-
-  graph = new Graph({
+  graph.value = new Graph({
     container: containerRef.value,
+    interacting: false,
     autoResize: true,
-    background: { color: '#0B1120' }, // 建议配合高科技风格改为暗色背景
-    grid: {
-      visible: true,
-      type: 'dot',
-      size: 20,
-      args: { color: '#1F3A5F', thickness: 2 } // 网格也调整为暗色系
-    },
-    mousewheel: false
+    panning: true,
+    mousewheel: { enabled: true, zoomAtMousePosition: true, modifiers: ['ctrl', 'meta'] },
+    background: { color: 'transparent' },
+    grid: { size: 10, visible: true, type: 'dot', args: { color: '#1E2E4A', thickness: 1 } }
   })
 
-  // 4. 遍历数据批量生成节点
-  nodesData.forEach((node) => {
-    graph!.addNode({
-      id: node.id,
-      shape: 'custom-node',
-      x: node.x,
-      y: node.y,
-      width: 150,
-      height: 160,
-      attrs: {
-        body: commonNodeAttrs.body,
-        icon: { ...commonNodeAttrs.icon, d: node.icon },
-        title: { ...commonNodeAttrs.title, text: node.title }
-      }
-    })
+  graph.value.on('node:mouseenter', ({ node, e }) => {
+    hoveredNodeId.value = node.id
+    nodeTooltip.x = e.clientX + 15
+    nodeTooltip.y = e.clientY + 15
+    nodeTooltip.visible = true
   })
-  edgesData.forEach((edge) => {
-    graph!.addEdge({
-      // 1. 核心优化:固定线条的起点和终点位置
-      source: {
-        cell: edge.source,
-        anchor: 'right' // 强制从右侧出
-      },
-      target: {
-        cell: edge.target,
-        anchor: 'left' // 强制从左侧入
-      },
-
-      // 2. 核心优化:使用曼哈顿路由规划线条走向,避免斜线穿插
-      router: {
-        name: 'manhattan',
-        args: {
-          padding: 30, // 线条拐弯处距离节点的缓冲间距
-          startDirections: ['right'], // 限制首段线条向右
-          endDirections: ['left'] // 限制末段线条向左
-        }
-      },
-
-      // 3. 连接器:在直角拐弯处增加大圆角,完美代替原先的 smooth,保留流体感
-      connector: {
-        name: 'rounded',
-        args: { radius: 24 } // 圆角半径可以根据喜好调大调小
-      },
-      markup: [
-        {
-          tagName: 'path',
-          selector: 'line'
-        }
-      ],
+  graph.value.on('node:mouseleave', () => {
+    hoveredNodeId.value = null
+    nodeTooltip.visible = false
+  })
+
+  graph.value.on('node:detail', () => {
+    if (airDialogRef.value) {
+      airDialogRef.value.handleOpen()
+    }
+  })
+}
+
+const renderEdges = () => {
+  edgeDatas.forEach((edgeData) => {
+    const edge = graph.value?.addEdge({ ...edgeData, ...edgeAttrs })
+
+    const deviceId = edgeData.id.replace('-edge', '')
+    const dev = deviceData.find((d) => d.id === deviceId)
+
+    if (dev && dev.status === 'stopped' && edge) {
+      edge.attr('line/style/animation', 'none')
+      edge.attr('fill/stroke', '#B50F0F')
+    } else if (edge) {
+      edge.attr('line/style/animation', 'edge-dash-flow 1.2s infinite linear')
+      edge.attr('fill/stroke', '#0F5BB5')
+    }
+  })
+}
+
+const renderNodes = () => {
+  deviceData.forEach((n) => {
+    graph.value?.addNode({
+      id: n.id,
+      x: n.x,
+      y: n.y,
+      width: n.width,
+      height: n.height,
+      shape: 'image',
+      imageUrl: `src/views/test/image/${n.img}`,
+      markup: nodeWithTitleMarkup,
       attrs: {
-        line: {
-          connection: true,
-          strokeWidth: 5,
-          strokeLinecap: 'round',
-
-          targetMarker: {
-            name: 'classic',
-            size: 16,
-            fill: '#00EAFF',
-            stroke: 'none',
-            offset: -4
-          },
-
-          strokeDasharray: '10, 20',
-          fill: 'none',
-
-          stroke: {
-            type: 'linearGradient',
-            stops: [
-              { offset: '0%', color: '#0A1E3C', opacity: 0.8 }, // 起点:与节点背景融合的暗色
-              { offset: '50%', color: '#0077FF', opacity: 0.8 }, // 中段:纯正科技蓝
-              { offset: '100%', color: '#00EAFF', opacity: 1 } // 终点:高亮青色,能量注入箭头
-            ]
-          }
-        }
-      },
-
-      animation: [
-        [
-          {
-            'attrs/line/opacity': [0.7, 1]
-          },
-          {
-            duration: 1000,
-            fill: 'forwards',
-            direction: 'alternate',
-            iterations: Infinity
-          }
-        ],
-        [
-          {
-            'attrs/line/strokeDashoffset': [30, 0]
-          },
-          {
-            duration: 500, // 800ms 匀速流动,不会太晃眼
-            iterations: Infinity
-          }
-        ]
-      ]
+        image: { preserveAspectRatio: 'none', refWidth: '100%', refHeight: '100%', event: n.event },
+        ...getTechTextAttrs(n.name, { width: 170, refX: n.refX, refY: n.refY })
+      }
     })
   })
+}
+
+onMounted(() => {
+  initGraph()
+  renderGroundAndFence()
+  renderEdges()
+  renderNodes()
+
+  if (graph.value) {
+    graph.value.zoomTo(0.35)
+    graph.value.translate(437, 530.65)
+  }
+  startRealtimeSimulation()
 })
 
-// 5. 页面销毁时清理画布,释放内存
 onUnmounted(() => {
-  if (graph) {
-    graph.dispose()
-  }
+  graph.value?.dispose()
+  if (timer) clearInterval(timer)
 })
 </script>
 
 <template>
   <div
-    class="h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="relative flex flex-col w-full overflow-hidden bg-[#0B1120] font-sans"
+    style="
+      height: calc(
+        100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+      );
+    "
   >
-    <div
-      class="w-full h-full border-1 border-solid border-gray-300 rounded-lg shadow"
-      ref="containerRef"
-    ></div>
+    <header
+      class="relative z-20 flex items-center justify-center h-16 shrink-0 bg-[#0B1120] border-b border-[#00E5FF]/40 shadow-[0_4px_20px_rgba(0,229,255,0.15)]"
+    >
+      <div
+        class="absolute left-10 right-10 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-[#00E5FF] to-transparent opacity-80"
+      ></div>
+      <h1
+        class="text-2xl font-bold tracking-[0.2em] text-[#00E5FF] drop-shadow-[0_0_10px_rgba(0,229,255,0.8)]"
+      >
+        XX注气场站注气工艺组态图
+      </h1>
+    </header>
+
+    <div class="relative flex-1 w-full h-full overflow-hidden">
+      <main class="absolute inset-0 z-0" ref="containerRef"></main>
+
+      <div class="absolute inset-0 z-10 pointer-events-none flex justify-between p-6">
+        <div class="flex flex-col gap-6 w-[380px] pointer-events-auto h-full pb-2">
+          <div
+            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md transition-all shrink-0"
+          >
+            <h2
+              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
+              >场站实时总览</h2
+            >
+            <div class="grid grid-cols-2 gap-4">
+              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                <span class="text-xs text-slate-300 font-medium mb-1">注气总流量 (Nm³/h)</span>
+                <span
+                  class="text-xl font-bold font-mono text-[#00FF7F] drop-shadow-[0_0_5px_rgba(0,255,127,0.5)] transition-all"
+                  >{{ overviewData.totalFlow.toLocaleString() }}</span
+                >
+              </div>
+              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                <span class="text-xs text-slate-300 font-medium mb-1">管网压力 (MPa)</span>
+                <span
+                  class="text-xl font-bold font-mono text-[#38BDF8] drop-shadow-[0_0_5px_rgba(56,189,248,0.5)] transition-all"
+                  >{{ overviewData.pressure }}</span
+                >
+              </div>
+              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                <span class="text-xs text-slate-300 font-medium mb-1">排气温度 (°C)</span>
+                <span
+                  class="text-xl font-bold font-mono text-[#FBBF24] drop-shadow-[0_0_5px_rgba(251,191,36,0.5)] transition-all"
+                  >{{ overviewData.temperature }}</span
+                >
+              </div>
+              <div class="bg-[#112240] border border-[#1E3A8A] rounded p-3 flex flex-col">
+                <span class="text-xs text-slate-300 font-medium mb-1">系统能效 (%)</span>
+                <span
+                  class="text-xl font-bold font-mono text-[#00E5FF] drop-shadow-[0_0_5px_rgba(0,229,255,0.5)] transition-all"
+                  >{{ overviewData.efficiency }}</span
+                >
+              </div>
+            </div>
+          </div>
+
+          <div
+            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col flex-1 min-h-0 backdrop-blur-md"
+          >
+            <h2
+              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)] shrink-0"
+              >全场设备状态监控</h2
+            >
+
+            <div class="flex-1 flex flex-col gap-3 overflow-y-auto pr-2 custom-scrollbar">
+              <div
+                v-for="dev in deviceData"
+                :key="dev.id"
+                class="flex items-center justify-between p-3 bg-[#112240] border border-[#1E3A8A] rounded hover:border-[#00E5FF]/60 transition-colors"
+              >
+                <div class="flex items-center gap-3">
+                  <div
+                    :class="[
+                      'w-3 h-3 rounded-full shadow-[0_0_8px] transition-colors shrink-0',
+                      dev.status === 'running'
+                        ? 'bg-[#00FF7F] shadow-[#00FF7F]'
+                        : 'bg-[#EF4444] shadow-[#EF4444]'
+                    ]"
+                  ></div>
+                  <span class="text-sm font-bold text-white w-[75px] truncate" :title="dev.name">{{
+                    dev.name
+                  }}</span>
+                </div>
+
+                <div class="flex gap-3 text-sm font-mono min-w-[120px] justify-end items-center">
+                  <div class="flex gap-3 min-w-[90px] justify-end">
+                    <div
+                      v-for="(param, idx) in dev.params.slice(0, 2)"
+                      :key="idx"
+                      class="flex flex-col items-end"
+                    >
+                      <span class="text-slate-400 text-[10px]">{{ param.label }}</span>
+                      <span
+                        :class="
+                          dev.status === 'running'
+                            ? 'text-[#00E5FF] font-bold transition-all'
+                            : 'text-slate-500'
+                        "
+                      >
+                        {{ param.value }}<span class="text-[10px] ml-0.5">{{ param.unit }}</span>
+                      </span>
+                    </div>
+                  </div>
+                  <button
+                    @click="toggleDevice(dev)"
+                    class="ml-2 p-1.5 rounded flex items-center justify-center transition-all duration-300 border focus:outline-none active:scale-90"
+                    :class="
+                      dev.status === 'running'
+                        ? 'border-[#EF4444]/30 text-[#EF4444] hover:bg-[#EF4444]/20 hover:border-[#EF4444] hover:shadow-[0_0_10px_rgba(239,68,68,0.5)]'
+                        : 'border-[#00FF7F]/30 text-[#00FF7F] hover:bg-[#00FF7F]/20 hover:border-[#00FF7F] hover:shadow-[0_0_10px_rgba(0,255,127,0.5)]'
+                    "
+                    :title="dev.status === 'running' ? '停止设备' : '启动设备'"
+                  >
+                    <div
+                      :class="dev.status === 'running' ? 'i-mdi-pause' : 'i-mdi-play'"
+                      class="w-4 h-4"
+                    >
+                    </div>
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="flex flex-col gap-6 w-[380px] pointer-events-auto h-full pb-2">
+          <div
+            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md shrink-0"
+          >
+            <h2
+              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
+              >注气井实时数据</h2
+            >
+            <div class="flex flex-col gap-4">
+              <div
+                class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
+              >
+                <span class="text-white text-sm font-medium">井口压力</span>
+                <span class="text-2xl font-mono text-[#00E5FF] font-bold transition-all"
+                  >{{ wellData.wellheadPressure
+                  }}<span class="text-sm text-slate-400">MPa</span></span
+                >
+              </div>
+              <div
+                class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
+              >
+                <span class="text-white text-sm font-medium">日注气量</span>
+                <span class="text-2xl font-mono text-[#00FF7F] font-bold transition-all"
+                  >{{ wellData.dailyInjection.toLocaleString()
+                  }}<span class="text-sm text-slate-400">Nm³</span></span
+                >
+              </div>
+              <div
+                class="flex justify-between items-end bg-[#112240] p-3 rounded border border-[#1E3A8A]"
+              >
+                <span class="text-white text-sm font-medium">累计注气</span>
+                <span class="text-2xl font-mono text-[#FBBF24] font-bold transition-all"
+                  >{{ wellData.cumulative }} <span class="text-sm text-slate-400">万Nm³</span></span
+                >
+              </div>
+            </div>
+          </div>
+
+          <div
+            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col backdrop-blur-md shrink-0"
+          >
+            <h2
+              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)]"
+              >气体成分占比</h2
+            >
+            <div class="flex flex-col gap-4">
+              <div v-for="gas in gasComposition" :key="gas.name" class="flex flex-col gap-2">
+                <div class="flex justify-between text-sm">
+                  <span class="text-white font-medium">{{ gas.name }}</span>
+                  <span class="text-[#00E5FF] font-mono font-bold transition-all"
+                    >{{ gas.value }}%</span
+                  >
+                </div>
+                <div class="w-full h-2 bg-[#1E3A8A] rounded-full overflow-hidden">
+                  <div
+                    :class="[
+                      'h-full rounded-full transition-all duration-500 ease-in-out',
+                      gas.color
+                    ]"
+                    :style="{ width: `${gas.value}%` }"
+                  ></div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div
+            class="bg-[#0A1628]/95 border border-[#00E5FF]/60 shadow-[0_0_15px_rgba(0,229,255,0.15)_inset] rounded-lg p-5 flex flex-col flex-1 min-h-0 backdrop-blur-md"
+          >
+            <h2
+              class="text-lg font-bold text-[#00E5FF] border-b border-[#00E5FF]/30 pb-2 mb-4 drop-shadow-[0_0_5px_rgba(0,229,255,0.8)] shrink-0"
+              >系统操作/告警日志</h2
+            >
+            <div class="flex-1 flex flex-col gap-3 overflow-y-auto pr-2 custom-scrollbar">
+              <div
+                v-for="(log, idx) in alarmLogs"
+                :key="idx"
+                class="flex flex-col p-3 bg-[#112240] rounded border-l-4"
+                :class="log.level === 'warn' ? 'border-[#FBBF24]' : 'border-[#00E5FF]'"
+              >
+                <div class="flex justify-between items-center mb-2">
+                  <span class="text-xs text-slate-400 font-mono">{{ log.time }}</span>
+                  <span
+                    :class="[
+                      'text-xs px-2 py-0.5 rounded font-bold',
+                      log.level === 'warn'
+                        ? 'bg-[#FBBF24]/20 text-[#FBBF24]'
+                        : 'bg-[#00E5FF]/20 text-[#00E5FF]'
+                    ]"
+                  >
+                    {{ log.level === 'warn' ? '警告' : '信息' }}
+                  </span>
+                </div>
+                <span class="text-sm text-white">{{ log.msg }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
+
+  <template v-if="hoveredNodeId">
+    <template v-for="dev in deviceData" :key="dev.id">
+      <div
+        v-if="dev.id === hoveredNodeId && nodeTooltip.visible"
+        class="coord-tooltip"
+        :style="{ left: `${nodeTooltip.x}px`, top: `${nodeTooltip.y}px`, minWidth: '200px' }"
+      >
+        <div
+          style="
+            padding-bottom: 6px;
+            margin-bottom: 4px;
+            font-size: 14px;
+            font-weight: bold;
+            color: #fff;
+            border-bottom: 1px dashed rgb(14 165 233 / 40%);
+          "
+        >
+          {{ dev.name }}
+        </div>
+        <div class="coord-item">
+          <span>运行状态:</span>
+          <span class="val" :style="{ color: dev.status === 'running' ? '#00FF7F' : '#ef4444' }">
+            {{ dev.status === 'running' ? '运行中' : '已停机' }}
+          </span>
+        </div>
+        <div class="coord-item" v-for="(param, index) in dev.params" :key="index">
+          <span>{{ param.label }}:</span>
+          <span class="val transition-all">{{ param.value }} {{ param.unit }}</span>
+        </div>
+      </div>
+    </template>
+  </template>
+
+  <air ref="airDialogRef" />
 </template>
 
-<style scoped></style>
+<style scoped>
+.transition-all {
+  transition: all 0.3s ease;
+}
+
+.coord-tooltip {
+  position: fixed;
+  z-index: 9999;
+  display: flex;
+  padding: 12px 16px;
+  font-family: monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #fff;
+  white-space: nowrap;
+  pointer-events: none;
+  background: rgb(10 22 40 / 95%);
+  border: 1px solid rgb(0 229 255 / 80%);
+  border-radius: 6px;
+  box-shadow:
+    0 4px 15px rgb(0 0 0 / 80%),
+    0 0 10px rgb(0 229 255 / 30%) inset;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.coord-item {
+  display: flex;
+  justify-content: space-between;
+  gap: 24px;
+}
+
+.coord-item .val {
+  font-weight: bold;
+  color: #00e5ff;
+}
+
+.custom-scrollbar::-webkit-scrollbar {
+  width: 6px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-track {
+  background: rgb(17 34 64 / 80%);
+  border-radius: 4px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb {
+  background: rgb(0 229 255 / 60%);
+  border-radius: 4px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb:hover {
+  background: rgb(0 229 255 / 100%);
+}
+</style>
+
+<style>
+@keyframes edge-dash-flow {
+  0% {
+    stroke-dashoffset: 50;
+  }
+
+  100% {
+    stroke-dashoffset: 0;
+  }
+}
+</style>

Някои файлове не бяха показани, защото твърде много файлове са промени