Explorar o código

调整空气处理橇组态

Zimo hai 6 horas
pai
achega
b001c4c683
Modificáronse 1 ficheiros con 274 adicións e 185 borrados
  1. 274 185
      src/views/test/air.vue

+ 274 - 185
src/views/test/air.vue

@@ -1,10 +1,12 @@
 <script setup lang="ts">
-import { ref, onUnmounted } from 'vue'
+import { ref, onUnmounted, reactive, nextTick } from 'vue'
 import { Graph } from '@antv/x6'
 
 const visible = ref(false)
 
 const psacontainerRef = ref<HTMLDivElement>()
+const wrapperRef = ref<HTMLDivElement>()
+const targetArea = ref<HTMLDivElement>()
 const graph = ref<Graph | null>(null)
 
 const hoveredNodeId = ref<string | null>(null)
@@ -15,19 +17,34 @@ const nodeTooltip = reactive({
   y: 0
 })
 
+const isRunning = ref(true)
+
+const globalParams = reactive([
+  { label: '处理橇出口温度', value: 6.7, unit: '℃' },
+  { label: '处理橇出口压力', value: 0.62, unit: 'MPa' },
+  { label: '预冷机进口温度', value: 15.1, unit: '℃' },
+  { label: '冷媒低压压力', value: 4.88, unit: 'bar' },
+  { label: '冷媒高压压力', value: 4.63, unit: 'bar' },
+  { label: '预冷机蒸发温度', value: 7.2, unit: '℃' },
+  { label: '预冷机冷凝温度', value: 47.0, unit: '℃' }
+])
+
 const initGraph = () => {
   if (!psacontainerRef.value) return
 
   graph.value = new Graph({
     container: psacontainerRef.value,
-    interacting: true,
+    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 } }
+    // 调亮了网格的颜色,如果想显示网格可以将 visible 改为 true
+    grid: { size: 2, visible: false, type: 'dot', args: { color: '#2A4D8F', thickness: 1 } }
   })
 
+  graph.value?.translate(114, 24)
+
   graph.value.on('node:mouseenter', ({ node, e }) => {
     hoveredNodeId.value = node.id
     nodeTooltip.x = e.clientX + 15
@@ -101,13 +118,14 @@ const nodesData = reactive<any>([
     name: '预冷机',
     status: 'running',
     params: [
-      { label: '进口温度', value: 38.5, unit: '°C' },
-      { label: '出口温度', value: 12.0, unit: '°C' },
-      { label: '冷媒压力', value: 0.85, unit: 'MPa' }
+      { label: '预冷机进口温度', value: 15.1, unit: '℃' },
+      { label: '冷媒低压压力', value: 4.88, unit: 'bar' },
+      { label: '冷媒高压压力', value: 4.63, unit: 'bar' },
+      { label: '预冷机蒸发温度', value: 7.2, unit: '℃' },
+      { label: '预冷机冷凝温度', value: 47.0, unit: '℃' }
     ]
   },
 
-  // 阀门不需要显示 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 },
@@ -141,13 +159,11 @@ const nodesData = reactive<any>([
     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,
@@ -155,14 +171,13 @@ const nodesData = reactive<any>([
     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,
@@ -170,7 +185,6 @@ const nodesData = reactive<any>([
     width: 80,
     height: 70,
     img: 'meter2.png',
-    name: '压力变送器',
     status: 'running',
     params: [
       { label: '空气压力', value: 0.75, unit: 'MPa' },
@@ -179,53 +193,70 @@ const nodesData = reactive<any>([
   }
 ])
 
-// 记录定时器,方便组件销毁时清理
 let simTimer: ReturnType<typeof setInterval> | null = null
 
-// 模拟实时数据波动
 const startSimulation = () => {
-  // 每 2 秒刷新一次数据
+  // 1. 预先获取并缓存常用引用,避免在定时器内重复查找
+  const coolNode = nodesData.find((dev: any) => dev.id === 'cool')
+
+  // 2. 建立 globalParams 到 coolNode.params 的快速映射 (如果需要频繁同步)
+  const coolParamMap = new Map()
+  if (coolNode?.params) {
+    coolNode.params.forEach((p: any) => coolParamMap.set(p.label, p))
+  }
+
+  // 辅助函数:处理数值波动
+  const getNewValue = (base: number, range: number, precision: number) => {
+    const change = (Math.random() * 2 - 1) * (base * range)
+    return Number((base + change).toFixed(precision))
+  }
+
   simTimer = setInterval(() => {
+    if (!isRunning.value) return
+
+    // 优化 1: 处理普通节点
     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))
-          }
-        })
+      // 过滤不需要模拟的节点
+      if (dev.id === 'cool' || !dev.params?.length) return
+
+      dev.params.forEach((param: any) => {
+        if (typeof param.value === 'string') return
+
+        // 特殊逻辑:累计流量(只增不减)
+        if (param.label === '累计流量') {
+          param.value = Number((param.value + (Math.random() * 0.4 + 0.1)).toFixed(1))
+          return
+        }
+
+        // 初始化基准值
+        if (param.baseValue === undefined) param.baseValue = param.value
+
+        // 根据单位决定精度和波动幅度
+        const unit = param.unit
+        if (unit === 'rpm') {
+          param.value = Math.round(getNewValue(param.baseValue, 0.02, 0))
+        } else if (['A', '°C', 'Nm³/h', '℃'].includes(unit)) {
+          param.value = getNewValue(param.baseValue, 0.02, 1)
+        } else {
+          param.value = getNewValue(param.baseValue, 0.02, 2)
+        }
+      })
+    })
+
+    // 优化 2: 处理全局参数并同步到 cool 节点
+    globalParams.forEach((param: any) => {
+      if (param.baseValue === undefined) param.baseValue = param.value
+
+      const newValue = getNewValue(param.baseValue, 0.01, 2)
+      param.value = newValue
+
+      // 直接通过 Map 快速更新,避免嵌套循环
+      const targetParam = coolParamMap.get(param.label)
+      if (targetParam) {
+        targetParam.value = newValue
       }
     })
-  }, 2000) // 2000毫秒(2秒)更新一次
+  }, 2000)
 }
 
 const renderNodes = () => {
@@ -264,7 +295,7 @@ const renderNodes = () => {
   })
 }
 
-const edgeAttrs = {
+const createEdgeAttrs = (color: string) => ({
   markup: [
     { tagName: 'path', selector: 'fill' },
     { tagName: 'path', selector: 'line' }
@@ -280,47 +311,21 @@ const edgeAttrs = {
     line: {
       connection: true,
       strokeWidth: 4,
-      stroke: '#0F5BB5',
+      stroke: color,
       strokeDasharray: '20, 30',
       strokeLinecap: 'round',
       targetMarker: null,
       fill: 'none',
-      filter: 'drop-shadow(0 0 3px #0F5BB5)',
+      filter: `drop-shadow(0 0 3px ${color})`,
       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: '#fcfeff',
-      strokeLinecap: 'round',
-      fill: 'none'
-    },
-    line: {
-      connection: true,
-      strokeWidth: 4,
-      stroke: '#2bbbc6',
-      strokeDasharray: '20, 30',
-      strokeLinecap: 'round',
-      targetMarker: null,
-      fill: 'none',
-      filter: 'drop-shadow(0 0 3px #2bbbc6)',
-      style: {
-        animation: 'edge-dash-flow 1.2s infinite linear'
-      }
-    }
-  }
-}
+const edgeAttrs = createEdgeAttrs('#0F5BB5')
+const edgeAttrs2 = createEdgeAttrs('#2bbbc6')
 
 function gendertargetMarker(
   type: 'blue' | 'green',
@@ -528,68 +533,65 @@ const edgeDatas = reactive<any>([
 ])
 
 const renderEdges = () => {
-  edgeDatas.forEach((edgeData) => {
-    const { type = 1, targetMarker, ...other } = edgeData,
-      attrs = type === 1 ? edgeAttrs : edgeAttrs2
+  edgeDatas.forEach((edgeData: any) => {
+    const { type = 1, targetMarker, ...other } = edgeData
+    const attrs = type === 1 ? edgeAttrs : edgeAttrs2
     const edge = graph.value?.addEdge({ ...other, ...attrs })
-    if (targetMarker) {
-      edge?.attr('line/targetMarker', targetMarker)
-    }
+    if (targetMarker) edge?.attr('line/targetMarker', targetMarker)
   })
 }
 
-// 新增外层容器和目标缩放区域 Ref
-const wrapperRef = ref<HTMLDivElement>()
-const targetArea = ref<HTMLDivElement>()
+const updateEdgesAnimation = (state: 'running' | 'paused') => {
+  if (!graph.value) return
+  const edges = graph.value.getEdges()
+  edges.forEach((edge) => {
+    edge.attr('line/style/animationPlayState', state)
+  })
+}
+
+const handleStart = () => {
+  isRunning.value = true
+  graph.value?.translate()
+  updateEdgesAnimation('running')
+}
+
+const handleStop = () => {
+  isRunning.value = false
+  updateEdgesAnimation('paused')
+}
 
 const updateScale = () => {
   if (!wrapperRef.value || !targetArea.value) return
-
   const currentWidth = wrapperRef.value.clientWidth
   const currentHeight = wrapperRef.value.clientHeight
-
-  // 针对你的节点坐标设定一个刚好能包住的虚拟画布大小
   const designWidth = 2200
   const designHeight = 1200
-
-  const scaleX = currentWidth / designWidth
-  const scaleY = currentHeight / designHeight
-  const scale = Math.min(scaleX, scaleY)
-
+  const scale = Math.min(currentWidth / designWidth, currentHeight / designHeight)
   targetArea.value.style.transform = `scale(${scale})`
 }
 
 function handleOpen() {
   visible.value = true
-
   nextTick(() => {
     initGraph()
     renderEdges()
     renderNodes()
-
     startSimulation()
+    updateScale()
+    window.addEventListener('resize', updateScale)
   })
-
-  window.addEventListener('resize', updateScale)
-}
-
-const handleOpened = () => {
-  updateScale()
 }
 
 function dispose() {
   graph.value?.dispose()
   window.removeEventListener('resize', updateScale)
-
   if (simTimer) {
     clearInterval(simTimer)
     simTimer = null
   }
 }
 
-defineExpose({
-  handleOpen
-})
+defineExpose({ handleOpen })
 
 onUnmounted(() => {
   dispose()
@@ -609,81 +611,113 @@ function handleClose() {
     v-model="visible"
     :show-close="false"
     @closed="dispose"
-    @opened="handleOpened"
   >
     <div
-      ref="wrapperRef"
-      class="flex justify-center items-center w-full h-full overflow-hidden bg-transparent"
+      class="psa-theme-bg relative size-full overflow-hidden font-sans rounded-lg flex flex-col border border-solid border-1 border-[#1e4485] shadow-[0_0_30px_rgba(0,229,255,0.1)]"
     >
-      <div
-        ref="targetArea"
-        class="relative shrink-0 overflow-hidden bg-[#0B1120] font-sans rounded-lg flex flex-col shadow-[0_0_30px_rgba(0,229,255,0.2)]"
-        style="width: 2200px; height: 1200px; transform-origin: center center"
+      <button
+        @click="handleClose"
+        class="absolute right-4 top-4 z-50 flex items-center justify-center w-8 h-8 bg-[#112b59]/80 rounded-full border border-solid border-1 border-[#1e4485] text-gray-300 hover:text-white hover:bg-[#1d4485] transition-all group focus:outline-none shadow-[0_0_10px_rgba(0,0,0,0.5)] backdrop-blur-md"
+        title="关闭"
       >
-        <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="i-ep-close w-5 h-5 group-hover:rotate-90 transition-all duration-300"></div>
+      </button>
+
+      <main class="flex-1 relative w-full h-full z-0 flex">
+        <div
+          ref="wrapperRef"
+          class="flex-1 relative h-full bg-transparent overflow-hidden flex justify-center items-center"
         >
           <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)]"
+            ref="targetArea"
+            class="relative shrink-0 overflow-hidden bg-transparent font-sans"
+            style="width: 2200px; height: 1200px; transform-origin: center center"
           >
-            空气处理橇组态图
-          </h1>
+            <div ref="psacontainerRef" class="w-full h-full"></div>
+          </div>
 
-          <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="absolute left-6 bottom-6 z-10 flex gap-4 items-center p-4 bg-[#0c2047]/85 border border-[#1e4485] rounded-lg shadow-[0_4px_20px_rgba(0,0,0,0.4)] backdrop-blur-md"
           >
-            <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%);
-                "
+            <div class="flex flex-col gap-3">
+              <button
+                class="btn-control bg-green-500/20 text-green-400 border border-green-500 hover:bg-green-500 hover:text-white"
+                @click="handleStart"
+                >冷干机风机启动</button
               >
-                {{ 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>
+              <button
+                class="btn-control bg-red-500/20 text-red-400 border border-red-500 hover:bg-red-500 hover:text-white"
+                @click="handleStop"
+                >冷干机风机停止</button
+              >
+            </div>
+            <div class="flex flex-col items-center justify-center ml-4 gap-2">
+              <div
+                class="w-8 h-8 rounded-full shadow-[0_0_10px_inset] transition-colors duration-300"
+                :class="isRunning ? 'bg-green-500 shadow-green-500' : 'bg-red-500 shadow-red-500'"
+              ></div>
+              <span class="text-white text-sm font-medium">运行指示</span>
+            </div>
+          </div>
+        </div>
+
+        <div
+          class="w-64 border-l border-[#1e4485] bg-[#0c2047]/85 flex flex-col gap-4 p-4 z-10 shrink-0 overflow-y-auto shadow-[-5px_0_20px_rgba(0,0,0,0.2)] backdrop-blur-md"
+        >
+          <div
+            class="text-[#00E5FF] font-bold text-base mt-1.2 mb-2 pb-2 border-b border-[#1e4485] flex items-center gap-2"
+          >
+            <i class="i-ep-data-line w-4 h-4"></i> 实时监测数据
+          </div>
+
+          <div class="param-card" v-for="(param, idx) in globalParams" :key="idx">
+            <div class="param-title">{{ param.label }}</div>
+            <div class="param-value-box">
+              <span class="val">{{ param.value }}</span>
+              <span class="unit">{{ param.unit }}</span>
             </div>
-          </template>
-        </template>
-      </div>
+          </div>
+        </div>
+      </main>
     </div>
+
+    <template v-if="hoveredNodeId">
+      <template v-for="dev in nodesData" :key="dev.id">
+        <div
+          v-if="
+            dev.id === hoveredNodeId && nodeTooltip.visible && dev.params && dev.params.length > 0
+          "
+          class="coord-tooltip"
+          :style="{ left: `${nodeTooltip.x}px`, top: `${nodeTooltip.y}px`, minWidth: '200px' }"
+        >
+          <div class="tooltip-header">{{ dev.name }}</div>
+          <div class="coord-item">
+            <span>运行状态:</span>
+            <span class="val" :style="{ color: isRunning ? '#00FF7F' : '#ef4444' }">
+              {{ isRunning ? '运行中' : '已停机' }}
+            </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>
   </el-dialog>
 </template>
 
 <style>
+@keyframes edge-dash-flow {
+  0% {
+    stroke-dashoffset: 50;
+  }
+
+  100% {
+    stroke-dashoffset: 0;
+  }
+}
+
 .psa-dialog {
   padding: 0;
   background-color: transparent;
@@ -696,19 +730,10 @@ function handleClose() {
 .psa-dialog .el-dialog__body {
   width: 100%;
   height: 100%;
-  padding: 0; /* 移除默认 padding,让背景铺满 */
-}
-
-@keyframes edge-dash-flow {
-  0% {
-    stroke-dashoffset: 50;
-  }
-
-  100% {
-    stroke-dashoffset: 0;
-  }
+  padding: 0;
 }
 
+/* Tooltip 改为通透的亮蓝色调 */
 .coord-tooltip {
   position: fixed;
   z-index: 9999;
@@ -720,14 +745,24 @@ function handleClose() {
   color: #fff;
   white-space: nowrap;
   pointer-events: none;
-  background: rgb(10 22 40 / 95%);
+  background: rgb(12 32 71 / 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;
+    0 0 15px rgb(0 229 255 / 30%) inset;
   flex-direction: column;
   gap: 6px;
+  backdrop-filter: blur(4px);
+}
+
+.tooltip-header {
+  padding-bottom: 6px;
+  margin-bottom: 4px;
+  font-size: 14px;
+  font-weight: bold;
+  color: #fff;
+  border-bottom: 1px dashed rgb(0 229 255 / 40%);
 }
 
 .coord-item {
@@ -740,4 +775,58 @@ function handleClose() {
   font-weight: bold;
   color: #00e5ff;
 }
+
+.param-card {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.param-title {
+  font-size: 13px;
+  color: #00e5ff;
+  text-align: center;
+}
+
+/* 数据框卡片改用亮眼的深蓝底色+内发光 */
+.param-value-box {
+  display: flex;
+  padding: 4px 12px;
+  background-color: #0c2047;
+  border: 1px solid #2bbbc6;
+  border-radius: 4px;
+  justify-content: space-between;
+  align-items: center;
+  box-shadow: inset 0 0 10px rgb(43 187 198 / 15%);
+}
+
+.param-value-box .val {
+  font-family: monospace;
+  font-size: 18px;
+  font-weight: bold;
+  color: #fbbf24;
+}
+
+.param-value-box .unit {
+  padding: 2px 6px;
+  font-size: 12px;
+  color: #fff;
+  background: #1e4485;
+  border-radius: 3px;
+}
+
+.btn-control {
+  padding: 6px 16px;
+  font-size: 14px;
+  font-weight: bold;
+  cursor: pointer;
+  border-radius: 4px;
+  outline: none;
+  transition: all 0.3s;
+}
+
+.psa-theme-bg {
+  /* background: radial-gradient(ellipse at top, #113066 0%, #091838 45%, #040b19 100%); */
+  background-color: #091838;
+}
 </style>