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