|
|
@@ -1,4 +1,4 @@
|
|
|
-<script setup lang="ts">
|
|
|
+<script setup lang="ts">
|
|
|
import { onMounted, onUnmounted, ref } from 'vue'
|
|
|
import * as THREE from 'three'
|
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
|
@@ -6,16 +6,20 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
|
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
|
|
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
|
|
|
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
|
|
|
-import { latLonToLocalENU, latLonToECEF, latLonToScenePosition, ecefToLatLon } from '../utils/geoCoord'
|
|
|
+import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
|
|
|
+import { latLonToLocalENU, latLonToECEF, ecefToLatLon } from '../utils/geoCoord'
|
|
|
import { createRimFlowMaterial } from '../materials/rimFlow'
|
|
|
import { createTechFloorMaterial } from '../materials/techFloor'
|
|
|
import xinjiangdibanGLB from '../assets/xinjiangdiban.glb'
|
|
|
+import markersConfig from '../config/markerConfig'
|
|
|
+import { useIconMarkers } from '../composables/useIconMarkers'
|
|
|
|
|
|
const containerRef = ref<HTMLDivElement>()
|
|
|
|
|
|
let scene: THREE.Scene
|
|
|
let camera: THREE.PerspectiveCamera
|
|
|
let renderer: THREE.WebGLRenderer
|
|
|
+let labelRenderer: CSS2DRenderer
|
|
|
let controls: OrbitControls
|
|
|
let composer: EffectComposer
|
|
|
let animationId: number
|
|
|
@@ -23,9 +27,12 @@ let animationId: number
|
|
|
let modelGroup: THREE.Group | null = null
|
|
|
|
|
|
/** 存储标记点位置,供聚焦按钮使用 */
|
|
|
-const markerPositions: Record<string, THREE.Vector3> = {}
|
|
|
+let markerPositions: Record<string, THREE.Vector3> = {}
|
|
|
const focusTarget = ref('原点')
|
|
|
|
|
|
+const iconMarkers = useIconMarkers(containerRef, ORIGIN_LAT, ORIGIN_LON)
|
|
|
+const { selectedMarker, popupStyle, closePopup } = iconMarkers
|
|
|
+
|
|
|
// 缓动函数
|
|
|
function easeInOutCubic(t: number): number {
|
|
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
|
|
|
@@ -90,7 +97,11 @@ function initScene() {
|
|
|
1,
|
|
|
500000,
|
|
|
)
|
|
|
- camera.position.set(100000, 80000, 150000)
|
|
|
+ // 初始视角:往左转30度 + 拉近
|
|
|
+ camera.position.set(-48000, 50000, 97000)
|
|
|
+
|
|
|
+ // 注入 scene / camera 到 composable
|
|
|
+ iconMarkers.init(scene, camera)
|
|
|
|
|
|
// Renderer
|
|
|
renderer = new THREE.WebGLRenderer({
|
|
|
@@ -104,21 +115,32 @@ function initScene() {
|
|
|
renderer.toneMappingExposure = 1.2
|
|
|
containerRef.value.appendChild(renderer.domElement)
|
|
|
|
|
|
- // Controls
|
|
|
+ // CSS2D Renderer(屏幕标签层)
|
|
|
+ labelRenderer = new CSS2DRenderer()
|
|
|
+ labelRenderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
|
|
|
+ labelRenderer.domElement.style.position = 'absolute'
|
|
|
+ labelRenderer.domElement.style.top = '0'
|
|
|
+ labelRenderer.domElement.style.left = '0'
|
|
|
+ labelRenderer.domElement.style.pointerEvents = 'none' // 让点击穿透
|
|
|
+ containerRef.value.appendChild(labelRenderer.domElement)
|
|
|
+
|
|
|
+ // Controls — 右键旋转,禁止平移
|
|
|
controls = new OrbitControls(camera, renderer.domElement)
|
|
|
controls.enableDamping = true
|
|
|
controls.dampingFactor = 0.1
|
|
|
+ controls.enablePan = false
|
|
|
+ controls.mouseButtons = { LEFT: null, MIDDLE: null, RIGHT: THREE.MOUSE.ROTATE }
|
|
|
controls.target.set(0, 0, 0)
|
|
|
|
|
|
- // Effect Composer (Bloom)
|
|
|
+ // Effect Composer (Bloom) — 低强度,只为模型加点光晕
|
|
|
composer = new EffectComposer(renderer)
|
|
|
const renderPass = new RenderPass(scene, camera)
|
|
|
composer.addPass(renderPass)
|
|
|
const bloomPass = new UnrealBloomPass(
|
|
|
new THREE.Vector2(containerRef.value.clientWidth, containerRef.value.clientHeight),
|
|
|
- 0.6, // strength
|
|
|
- 0.4, // radius
|
|
|
- 0.85, // threshold
|
|
|
+ 0.08, // strength — 很弱,不干扰线条
|
|
|
+ 0.2, // radius
|
|
|
+ 0.6, // threshold
|
|
|
)
|
|
|
composer.addPass(bloomPass)
|
|
|
|
|
|
@@ -143,11 +165,26 @@ function initScene() {
|
|
|
// ---------- 展示 geodesy 坐标转换结果 ----------
|
|
|
addGeodesyTestMarkers()
|
|
|
|
|
|
+ // ---------- 渲染配置中的图标标记 ----------
|
|
|
+ markerPositions = iconMarkers.addIconMarkers()
|
|
|
+
|
|
|
+ // ---------- 创建点间的 Shader 光带 (直连) ----------
|
|
|
+ addConnectionLines()
|
|
|
+
|
|
|
+ // 禁止右键菜单
|
|
|
+ renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault())
|
|
|
+
|
|
|
// Resize
|
|
|
window.addEventListener('resize', onResize)
|
|
|
|
|
|
// Start loop
|
|
|
animate()
|
|
|
+
|
|
|
+ // 点击空白处关闭弹窗
|
|
|
+ containerRef.value.addEventListener('click', (e) => {
|
|
|
+ if ((e.target as HTMLElement).closest('.popup-content')) return
|
|
|
+ iconMarkers.closePopup()
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
/** 加载 xinjiangdiban.glb */
|
|
|
@@ -214,19 +251,197 @@ function createTechFloor() {
|
|
|
|
|
|
let flowMaterials: THREE.ShaderMaterial[] = []
|
|
|
|
|
|
+/** 连线顺序:渠首→条形沉砂池→退水闸→2#沉砂池→1号沉砂池 */
|
|
|
+const CONNECTION_ORDER = ['渠首', '条形沉砂池', '退水闸', '2#沉砂池', '1#沉砂池']
|
|
|
+
|
|
|
+/** 第二条线:石门水库→莫勒切河→分水闸→1#沉砂池→团场 */
|
|
|
+const CONNECTION_ORDER_2 = ['石门水库', '莫勒切河', '分水闸', '1#沉砂池', '团场']
|
|
|
+
|
|
|
+// ---- 流动箭头光带 (Shader 实现) ----
|
|
|
+let arrowRibbonMaterials: THREE.ShaderMaterial[] = []
|
|
|
+
|
|
|
+const arrowVertexShader = `
|
|
|
+varying vec2 vUv;
|
|
|
+void main() {
|
|
|
+ vUv = uv;
|
|
|
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
|
+}
|
|
|
+`
|
|
|
+
|
|
|
+const arrowFragmentShader = `
|
|
|
+uniform float uTime;
|
|
|
+uniform vec3 uColor;
|
|
|
+
|
|
|
+varying vec2 vUv;
|
|
|
+
|
|
|
+void main() {
|
|
|
+ // vUv.x = 沿路径方向(0→1), vUv.y = 垂直方向(0→1, 中心0.5)
|
|
|
+
|
|
|
+ // 沿路径流动的脉冲波
|
|
|
+ float dx = vUv.x - uTime * 0.1;
|
|
|
+ float wave = sin(dx * 20.0) * 0.5 + 0.5;
|
|
|
+ // 细分脉冲波形成断续的段
|
|
|
+ float seg = floor(dx * 14.0);
|
|
|
+ float segPhase = fract(dx * 14.0);
|
|
|
+ float pulse = smoothstep(0.0, 0.05, segPhase) * (1.0 - smoothstep(0.4, 1.0, segPhase));
|
|
|
+ pulse *= 0.6 + 0.4 * wave;
|
|
|
+
|
|
|
+ // 垂直方向衰减 (中心亮, 边缘淡)
|
|
|
+ float dy = abs(vUv.y - 0.5) * 2.0;
|
|
|
+ float vertFalloff = 1.0 - smoothstep(0.0, 1.0, dy);
|
|
|
+
|
|
|
+ float alpha = pulse * vertFalloff * 0.7;
|
|
|
+ alpha = clamp(alpha, 0.0, 1.0);
|
|
|
+
|
|
|
+ vec3 color = uColor + vec3(0.2, 0.4, 0.6) * pulse;
|
|
|
+ gl_FragColor = vec4(color, alpha);
|
|
|
+}
|
|
|
+`
|
|
|
+
|
|
|
+/** 沿路径构建箭头光带 */
|
|
|
+function createFlowArrowRibbon(pts: THREE.Vector3[], width = 300, color = '#2196f3') {
|
|
|
+ if (pts.length < 2) return
|
|
|
+
|
|
|
+ const positions: number[] = []
|
|
|
+ const uvs: number[] = []
|
|
|
+
|
|
|
+ // 计算每段长度和总长
|
|
|
+ const segLens: number[] = []
|
|
|
+ let totalLen = 0
|
|
|
+ for (let i = 0; i < pts.length - 1; i++) {
|
|
|
+ const d = pts[i].distanceTo(pts[i + 1])
|
|
|
+ segLens.push(d)
|
|
|
+ totalLen += d
|
|
|
+ }
|
|
|
+
|
|
|
+ let uOffset = 0
|
|
|
+ for (let i = 0; i < pts.length - 1; i++) {
|
|
|
+ const p1 = pts[i]
|
|
|
+ const p2 = pts[i + 1]
|
|
|
+ const dir = new THREE.Vector3().copy(p2).sub(p1).normalize()
|
|
|
+ const up = new THREE.Vector3(0, 1, 0)
|
|
|
+ const side = new THREE.Vector3().crossVectors(dir, up)
|
|
|
+ if (side.length() < 0.001) {
|
|
|
+ side.crossVectors(dir, new THREE.Vector3(1, 0, 0))
|
|
|
+ }
|
|
|
+ side.normalize().multiplyScalar(width / 2)
|
|
|
+
|
|
|
+ const a = p1.clone().add(side)
|
|
|
+ const b = p1.clone().sub(side)
|
|
|
+ const c = p2.clone().add(side)
|
|
|
+ const d = p2.clone().sub(side)
|
|
|
+
|
|
|
+ const u0 = uOffset / totalLen
|
|
|
+ const u1 = (uOffset + segLens[i]) / totalLen
|
|
|
+
|
|
|
+ // 两个三角形: a-b-c, b-d-c
|
|
|
+ positions.push(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z)
|
|
|
+ positions.push(b.x, b.y, b.z, d.x, d.y, d.z, c.x, c.y, c.z)
|
|
|
+ uvs.push(u0, 1, u0, 0, u1, 1)
|
|
|
+ uvs.push(u0, 0, u1, 0, u1, 1)
|
|
|
+
|
|
|
+ uOffset += segLens[i]
|
|
|
+ }
|
|
|
+
|
|
|
+ const geo = new THREE.BufferGeometry()
|
|
|
+ geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3))
|
|
|
+ geo.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2))
|
|
|
+
|
|
|
+ const mat = new THREE.ShaderMaterial({
|
|
|
+ uniforms: {
|
|
|
+ uTime: { value: 0 },
|
|
|
+ uColor: { value: new THREE.Color(color) },
|
|
|
+ },
|
|
|
+ vertexShader: arrowVertexShader,
|
|
|
+ fragmentShader: arrowFragmentShader,
|
|
|
+ transparent: true,
|
|
|
+ side: THREE.DoubleSide,
|
|
|
+ depthWrite: false,
|
|
|
+ depthTest: false,
|
|
|
+ blending: THREE.AdditiveBlending,
|
|
|
+ })
|
|
|
+
|
|
|
+ const mesh = new THREE.Mesh(geo, mat)
|
|
|
+ mesh.renderOrder = 1000
|
|
|
+ scene.add(mesh)
|
|
|
+ arrowRibbonMaterials.push(mat)
|
|
|
+}
|
|
|
+
|
|
|
+/** 在两点间构建一段矩形条 Mesh (粗线) */
|
|
|
+function buildLineSegment(p1: THREE.Vector3, p2: THREE.Vector3, width: number, color: number): THREE.Mesh {
|
|
|
+ const dir = new THREE.Vector3().copy(p2).sub(p1).normalize()
|
|
|
+ const up = new THREE.Vector3(0, 1, 0)
|
|
|
+ const side = new THREE.Vector3().crossVectors(dir, up)
|
|
|
+ if (side.length() < 0.001) side.crossVectors(dir, new THREE.Vector3(1, 0, 0))
|
|
|
+ side.normalize().multiplyScalar(width / 2)
|
|
|
+
|
|
|
+ const a = p1.clone().add(side)
|
|
|
+ const b = p1.clone().sub(side)
|
|
|
+ const c = p2.clone().add(side)
|
|
|
+ const d = p2.clone().sub(side)
|
|
|
+
|
|
|
+ const pos = new Float32Array([
|
|
|
+ a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z,
|
|
|
+ b.x, b.y, b.z, d.x, d.y, d.z, c.x, c.y, c.z,
|
|
|
+ ])
|
|
|
+ const geo = new THREE.BufferGeometry()
|
|
|
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3))
|
|
|
+ const mat = new THREE.MeshBasicMaterial({
|
|
|
+ color,
|
|
|
+ side: THREE.DoubleSide,
|
|
|
+ depthWrite: false,
|
|
|
+ depthTest: false,
|
|
|
+ })
|
|
|
+ const mesh = new THREE.Mesh(geo, mat)
|
|
|
+ mesh.renderOrder = 999
|
|
|
+ return mesh
|
|
|
+}
|
|
|
+
|
|
|
+/** 通用:穿起指定标记点列表,画粗线 + 箭头光带 */
|
|
|
+function addRouteLine(order: string[], color: number, ribbonColor: string, ribbonWidth: number) {
|
|
|
+ const pts: THREE.Vector3[] = []
|
|
|
+ for (const label of order) {
|
|
|
+ const pos = markerPositions[label]
|
|
|
+ if (pos) {
|
|
|
+ const yOffset = (label === '渠首' || label === '条形沉砂池') ? 1200 : 500
|
|
|
+ pts.push(pos.clone().add(new THREE.Vector3(0, yOffset, 0)))
|
|
|
+ } else {
|
|
|
+ console.warn(`[连线] 未找到标记点: ${label}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (pts.length < 2) return
|
|
|
+
|
|
|
+ // 每段之间画粗线
|
|
|
+ for (let i = 0; i < pts.length - 1; i++) {
|
|
|
+ scene.add(buildLineSegment(pts[i], pts[i + 1], 600, color))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 箭头光带
|
|
|
+ createFlowArrowRibbon(pts, ribbonWidth, ribbonColor)
|
|
|
+}
|
|
|
+
|
|
|
+/** 创建所有线路 */
|
|
|
+function addConnectionLines() {
|
|
|
+ addRouteLine(CONNECTION_ORDER, 0x2196f3, '#2196f3', 800)
|
|
|
+ addRouteLine(CONNECTION_ORDER_2, 0x2196f3, '#2196f3', 800)
|
|
|
+ addRouteLine(['分水闸', '2#沉砂池'], 0x2196f3, '#2196f3', 800)
|
|
|
+}
|
|
|
+
|
|
|
function animate() {
|
|
|
animationId = requestAnimationFrame(animate)
|
|
|
|
|
|
- // 更新流光材质的 uTime
|
|
|
- if (flowMaterials.length > 0) {
|
|
|
- const time = performance.now() * 0.001
|
|
|
- flowMaterials.forEach((mat) => {
|
|
|
- mat.uniforms.uTime.value = time
|
|
|
- })
|
|
|
- }
|
|
|
+ // 更新所有箭头光带的 uTime
|
|
|
+ const time = performance.now() * 0.001
|
|
|
+ arrowRibbonMaterials.forEach((mat) => {
|
|
|
+ mat.uniforms.uTime.value = time
|
|
|
+ })
|
|
|
|
|
|
controls.update()
|
|
|
+
|
|
|
composer.render()
|
|
|
+
|
|
|
+ labelRenderer.render(scene, camera)
|
|
|
+ iconMarkers.updatePopupPosition()
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -323,6 +538,7 @@ function onResize() {
|
|
|
camera.aspect = width / height
|
|
|
camera.updateProjectionMatrix()
|
|
|
renderer.setSize(width, height)
|
|
|
+ labelRenderer.setSize(width, height)
|
|
|
}
|
|
|
|
|
|
function disposeScene() {
|
|
|
@@ -331,6 +547,10 @@ function disposeScene() {
|
|
|
controls.dispose()
|
|
|
if (composer) composer.dispose()
|
|
|
renderer.dispose()
|
|
|
+ iconMarkers.dispose()
|
|
|
+ if (labelRenderer) {
|
|
|
+ labelRenderer.domElement.remove()
|
|
|
+ }
|
|
|
if (containerRef.value && renderer.domElement.parentElement) {
|
|
|
containerRef.value.removeChild(renderer.domElement)
|
|
|
}
|
|
|
@@ -347,23 +567,79 @@ onUnmounted(() => {
|
|
|
|
|
|
<template>
|
|
|
<div ref="containerRef" class="scene-container">
|
|
|
- <div class="toolbar">
|
|
|
- <button
|
|
|
- v-for="pt in testPoints"
|
|
|
- :key="pt.label"
|
|
|
- class="btn"
|
|
|
- :class="{ active: focusTarget === pt.label }"
|
|
|
- @click="flyTo(pt.label)"
|
|
|
- >
|
|
|
- {{ pt.label }}
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="btn"
|
|
|
- :class="{ active: focusTarget === '38团' }"
|
|
|
- @click="flyTo('38团')"
|
|
|
- >
|
|
|
- 38团
|
|
|
- </button>
|
|
|
+ <!-- 图标点击弹窗 -->
|
|
|
+ <div
|
|
|
+ v-if="selectedMarker"
|
|
|
+ class="popup-overlay"
|
|
|
+ :style="popupStyle"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <div class="popup-content">
|
|
|
+ <div class="popup-header">
|
|
|
+ <div class="popup-title">{{ selectedMarker.label }}</div>
|
|
|
+ <div v-if="selectedMarker.info" class="popup-info">{{ selectedMarker.info }}</div>
|
|
|
+ <button class="popup-close" @click="closePopup">✕</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 闸门监控面板 (有 gateData 才显示) -->
|
|
|
+ <template v-if="selectedMarker.gateData">
|
|
|
+ <div class="gate-panel">
|
|
|
+ <!-- 闸门开度 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">闸门开度</span>
|
|
|
+ <span class="gate-value">{{ selectedMarker.gateData.opening.current }}/{{ selectedMarker.gateData.opening.max }}{{ selectedMarker.gateData.opening.unit }}</span>
|
|
|
+ <div class="gate-bar-wrap">
|
|
|
+ <div class="gate-bar" :style="{ width: selectedMarker.gateData.opening.percent + '%' }"></div>
|
|
|
+ </div>
|
|
|
+ <span class="gate-sub">目标 {{ selectedMarker.gateData.opening.target }}{{ selectedMarker.gateData.opening.unit }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 运行状态 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">运行状态</span>
|
|
|
+ <span class="gate-value" :class="'status-' + selectedMarker.gateData.status">{{ selectedMarker.gateData.status }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 限位信号 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">限位信号</span>
|
|
|
+ <span class="gate-tag" :class="selectedMarker.gateData.limitSignals.upper ? 'tag-on' : 'tag-off'">上限</span>
|
|
|
+ <span class="gate-tag" :class="selectedMarker.gateData.limitSignals.lower ? 'tag-on' : 'tag-off'">下限</span>
|
|
|
+ <span class="gate-tag" :class="selectedMarker.gateData.limitSignals.emergency ? 'tag-on tag-danger' : 'tag-off'">极限</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 荷重/拉力 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">荷重/拉力</span>
|
|
|
+ <span class="gate-value" :class="{ 'text-warning': selectedMarker.gateData.load.warning }">{{ selectedMarker.gateData.load.current }} t</span>
|
|
|
+ <span v-if="selectedMarker.gateData.load.warning" class="gate-tag tag-danger">过载预警</span>
|
|
|
+ <span v-if="selectedMarker.gateData.load.protected" class="gate-tag tag-danger">过载保护</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 左右同步 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">左右同步</span>
|
|
|
+ <span class="gate-value" :class="'sync-' + selectedMarker.gateData.sync.status">{{ selectedMarker.gateData.sync.status }}</span>
|
|
|
+ <span class="gate-sub">偏差 {{ selectedMarker.gateData.sync.deviation }} mm</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 水位 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">水位</span>
|
|
|
+ <span class="gate-sub">上游 {{ selectedMarker.gateData.waterLevel.upstream }}{{ selectedMarker.gateData.waterLevel.unit }}</span>
|
|
|
+ <span class="gate-sub">下游 {{ selectedMarker.gateData.waterLevel.downstream }}{{ selectedMarker.gateData.waterLevel.unit }}</span>
|
|
|
+ <span class="gate-sub" v-if="selectedMarker.gateData.waterLevel.diff">差 {{ selectedMarker.gateData.waterLevel.diff }}{{ selectedMarker.gateData.waterLevel.unit }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 流量 -->
|
|
|
+ <div class="gate-row">
|
|
|
+ <span class="gate-label">流量</span>
|
|
|
+ <span class="gate-value">{{ selectedMarker.gateData.flow.realtime }} {{ selectedMarker.gateData.flow.unit }}</span>
|
|
|
+ <span class="gate-sub">累计 {{ selectedMarker.gateData.flow.accumulated }} {{ selectedMarker.gateData.flow.unit }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
@@ -376,33 +652,133 @@ onUnmounted(() => {
|
|
|
overflow: hidden;
|
|
|
position: relative;
|
|
|
}
|
|
|
-.toolbar {
|
|
|
+
|
|
|
+/* 弹窗样式 */
|
|
|
+.popup-overlay {
|
|
|
position: absolute;
|
|
|
- top: 16px;
|
|
|
- left: 16px;
|
|
|
- z-index: 10;
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px;
|
|
|
+ z-index: 20;
|
|
|
+ transform: translate(-50%, -100%) translateY(-20px);
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+.popup-content {
|
|
|
+ position: relative;
|
|
|
+ background: rgba(10, 20, 40, 0.95);
|
|
|
+ border: 1px solid rgba(0, 150, 255, 0.5);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 0;
|
|
|
+ min-width: 240px;
|
|
|
+ max-width: 260px;
|
|
|
+ backdrop-filter: blur(12px);
|
|
|
+ box-shadow: 0 4px 24px rgba(0,0,0,0.6), 0 0 20px rgba(0,150,255,0.15);
|
|
|
}
|
|
|
-.btn {
|
|
|
- padding: 6px 14px;
|
|
|
- border: 1px solid rgba(255,255,255,0.3);
|
|
|
- border-radius: 6px;
|
|
|
- background: rgba(0,0,0,0.5);
|
|
|
- color: #ccc;
|
|
|
+.popup-header {
|
|
|
+ position: relative;
|
|
|
+ padding: 12px 36px 8px 14px;
|
|
|
+ border-bottom: 1px solid rgba(0,150,255,0.2);
|
|
|
+}
|
|
|
+.popup-title {
|
|
|
+ color: #64b5f6;
|
|
|
+ font: bold 14px "Microsoft YaHei", sans-serif;
|
|
|
+}
|
|
|
+.popup-info {
|
|
|
+ color: #aaa;
|
|
|
+ font: 11px "Microsoft YaHei", sans-serif;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+.popup-close {
|
|
|
+ position: absolute;
|
|
|
+ top: 6px;
|
|
|
+ right: 8px;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: #666;
|
|
|
cursor: pointer;
|
|
|
- font-size: 13px;
|
|
|
- backdrop-filter: blur(4px);
|
|
|
- transition: all 0.2s;
|
|
|
+ font-size: 16px;
|
|
|
+ padding: 2px 6px;
|
|
|
+ line-height: 1;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: all 0.15s;
|
|
|
}
|
|
|
-.btn:hover {
|
|
|
- background: rgba(255,255,255,0.15);
|
|
|
+.popup-close:hover {
|
|
|
color: #fff;
|
|
|
+ background: rgba(255,255,255,0.1);
|
|
|
}
|
|
|
-.btn.active {
|
|
|
- background: #2196f3;
|
|
|
- border-color: #2196f3;
|
|
|
- color: #fff;
|
|
|
+
|
|
|
+/* 闸门监控面板 */
|
|
|
+.gate-panel {
|
|
|
+ padding: 8px 14px 12px;
|
|
|
+}
|
|
|
+.gate-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 5px 0;
|
|
|
+ border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
+.gate-row:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+.gate-label {
|
|
|
+ color: #8899aa;
|
|
|
+ font: 11px "Microsoft YaHei", sans-serif;
|
|
|
+ min-width: 62px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.gate-value {
|
|
|
+ color: #8cf;
|
|
|
+ font: 12px/1.4 "Microsoft YaHei", sans-serif;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.gate-sub {
|
|
|
+ color: #778;
|
|
|
+ font: 10px "Microsoft YaHei", sans-serif;
|
|
|
+}
|
|
|
+.gate-bar-wrap {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 40px;
|
|
|
+ height: 4px;
|
|
|
+ background: rgba(255,255,255,0.08);
|
|
|
+ border-radius: 2px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.gate-bar {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, #2196f3, #64b5f6);
|
|
|
+ border-radius: 2px;
|
|
|
+ transition: width 0.3s;
|
|
|
+}
|
|
|
+.gate-tag {
|
|
|
+ font: 10px "Microsoft YaHei", sans-serif;
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ border: 1px solid transparent;
|
|
|
+}
|
|
|
+.tag-on {
|
|
|
+ color: #4caf50;
|
|
|
+ border-color: #4caf50;
|
|
|
+ background: rgba(76,175,80,0.1);
|
|
|
+}
|
|
|
+.tag-off {
|
|
|
+ color: #555;
|
|
|
+ border-color: #444;
|
|
|
+ background: rgba(255,255,255,0.03);
|
|
|
+}
|
|
|
+.tag-danger {
|
|
|
+ color: #f44336 !important;
|
|
|
+ border-color: #f44336 !important;
|
|
|
+ background: rgba(244,67,54,0.1) !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 状态颜色 */
|
|
|
+.status-稳定, .status-停止 { color: #4caf50; }
|
|
|
+.status-正在开启 { color: #2196f3; }
|
|
|
+.status-正在关闭 { color: #ff9800; }
|
|
|
+.status-卡滞 { color: #f44336; }
|
|
|
+
|
|
|
+.sync-同步 { color: #4caf50; }
|
|
|
+.sync-偏差 { color: #ff9800; }
|
|
|
+.sync-异常 { color: #f44336; }
|
|
|
+
|
|
|
+.text-warning { color: #ff9800 !important; }
|
|
|
</style>
|