|
|
@@ -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>
|