Ver código fonte

红沙泉cad模型布点逻辑修改-提交

lxh 1 dia atrás
pai
commit
df528abf81

+ 413 - 282
src/views/vent/cad/dwgViewer.vue

@@ -1,96 +1,117 @@
 <template>
+  <!-- 主内容容器 -->
   <div class="content">
+    <!-- CAD图纸查看区域 -->
     <div class="viewer-area">
-      <div :id="containerId" ref="viewerRef" class="viewer-canvas"></div>
+      <!-- DWG图纸渲染画布,id用于Viewer2d初始化 -->
+      <div :id="containerId" class="viewer-canvas"></div>
 
+      <!-- 坐标状态栏:显示鼠标在世界坐标系中的位置 -->
       <div v-if="viewerReady" class="coord-bar">
         <span>X: {{ currentCoord.x.toFixed(2) }}</span>
         <span>Y: {{ currentCoord.y.toFixed(2) }}</span>
-        <span v-if="placingMode" class="placing-hint">| 放置: {{ placingDevice?.name }} (Esc取消)</span>
+        <!-- 布点模式下显示提示信息,Esc可取消 -->
+        <span v-if="placingMode" class="placing-hint">| 放置: {{ placingDevice?.strname }} (Esc取消)</span>
       </div>
     </div>
 
+    <!-- 右侧设备列表面板,支持折叠展开 -->
     <div class="device-panel" :class="{ collapsed: panelCollapsed }">
+      <!-- 面板头部:点击切换展开/收起状态 -->
       <div class="panel-header" @click="panelCollapsed = !panelCollapsed">
         <span class="panel-title">设备列表</span>
         <span class="panel-toggle">{{ panelCollapsed ? '展开' : '收起' }}</span>
       </div>
+      <!-- 面板内容:设备表格,显示名称、状态、布点操作 -->
       <div v-show="!panelCollapsed" class="panel-body">
         <a-table :columns="columns" :data-source="devices" :pagination="false" row-key="id" size="small" :scroll="{ y: 360 }">
           <template #bodyCell="{ column, record }">
-            <template v-if="column.key === 'index'">{{ devices.indexOf(record) + 1 }}</template>
+            <!-- 状态列:根据netStatus显示在线/离线标签 -->
             <template v-if="column.key === 'netStatus'">
               <a-tag :color="statusColor(record.netStatus)">{{ statusText(record.netStatus) }}</a-tag>
             </template>
+            <!-- 操作列:未布点显示布点按钮,已布点显示已布点和撤销按钮 -->
             <template v-if="column.key === 'action'">
-              <a-button type="primary" size="small" :disabled="placingMode" @click="enterPlacingMode(record)">布点</a-button>
-            </template>
-            <template v-if="column.key === 'markerStatus'">
-              <a-tag v-if="record.markerId" color="green">已布</a-tag>
-              <span v-else style="color: rgba(255, 255, 255, 0.35); font-size: 12px">-</span>
+              <!-- <a-button
+                v-if="!isDevicePlaced(record)"
+                type="primary"
+                size="small"
+                :disabled="placingMode"
+                @click="enterPlacingMode(record)"
+              >布点</a-button> -->
+               <a-button
+                v-if="!isDevicePlaced(record)"
+                type="primary"
+                size="small"
+                @click="enterPlacingMode(record)"
+              >布点</a-button>
+              <template v-else>
+                <!-- <div style="display: flex; align-items: center; justify-content: center;">
+                  <a-button size="small" disabled style="margin-right: 4px">已布点</a-button>
+                  <a-button type="link" size="small" danger :disabled="placingMode" @click="revokePlacement(record)">撤销</a-button>
+                </div> -->
+                 <a-button type="link" size="small" primary :disabled="placingMode" @click="revokePlacement(record)">撤销</a-button>
+              </template>
             </template>
           </template>
         </a-table>
       </div>
     </div>
-
-    <a-modal v-model:open="modalVisible" :title="(monitoringData?.deviceName || '') + ' - 监测数据'" :footer="null" width="420px">
-      <div v-if="monitoringData" class="monitor-data">
-        <div class="monitor-row"
-          ><span class="monitor-key">时间戳</span><span>{{ monitoringData.timestamp }}</span></div
-        >
-        <div class="monitor-row"
-          ><span class="monitor-key">风速</span><span>{{ monitoringData.windSpeed }} m/s</span></div
-        >
-        <div class="monitor-row"
-          ><span class="monitor-key">瓦斯浓度</span><span>{{ monitoringData.gasConcentration }}%</span></div
-        >
-        <div class="monitor-row"
-          ><span class="monitor-key">温度</span><span>{{ monitoringData.temperature }}°C</span></div
-        >
-        <div class="monitor-row"
-          ><span class="monitor-key">压力</span><span>{{ monitoringData.pressure }} Pa</span></div
-        >
-        <div class="monitor-row"
-          ><span class="monitor-key">粉尘浓度</span><span>{{ monitoringData.dustDensity }} mg/m³</span></div
-        >
-        <div class="monitor-row"
-          ><span class="monitor-key">运行状态</span
-          ><a-tag :color="monitoringData.status === '正常' ? 'green' : 'red'">{{ monitoringData.status }}</a-tag></div
-        >
-      </div>
-    </a-modal>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, onUnmounted, reactive } from 'vue';
+  import { ref, onMounted, onUnmounted, reactive, toRef, watch } from 'vue';
   import { Viewer2d, ViewerEvent, CoordinateUtils, THREE } from '@x-viewer/core';
-  import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
   import { useAppStoreWithOut } from '/@/store/modules/app';
-  import dayjs from 'dayjs';
-
 
-  let props=defineProps({
+  const props = defineProps({
     devices: {
       type: Array,
       default: () => [],
     },
-  })
+  });
+  const deviceList = toRef(props, 'devices');
+
   // ==================== 类型 ====================
 
-  interface DeviceInfo {
-    id: string;
-    name: string;
-    type: string;
-    status: 'online' | 'offline' | 'alarm';
-    markerId: string | null;
+  interface DeviceRecord {
+    deviceID?: string;
+    id?: string;
+    strname?: string;
+    strinstallpos?: string;
+    netStatus?: number | string;
+    syswarnLevel?: number | string | boolean;
+    readData?: { temperature?: number | string };
+    markerId?: string | null;
+    [key: string]: any;
+  }
+
+  interface MarkerInfoData {
+    deviceName: string;
+    installPos: string;
+    runStatus: string;
+    alarmStatus: string;
+    temperature: string;
+  }
+
+  /** 持久化的布点记录 */
+  interface PersistedMarker {
+    deviceId: string;
+    x: number;
+    y: number;
   }
 
   // ==================== 常量 ====================
 
   const containerId = 'dwg-viewer-canvas';
   const dwgSrc = '/dwg/2026年2月总平面布置图_无高程.dwg';
+  /** 布点持久化存储 key(按图纸区分) */
+  const MARKER_STORAGE_KEY = 'dwg-viewer-markers:main-plan';
+  /** 正常状态布点颜色(绿色) */
+  const MARKER_COLOR_NORMAL = 0x52c41a;
+  /** 报警状态布点颜色(红色) */
+  const MARKER_COLOR_ALARM = 0xff4d4f;
   const fontFiles = [
     '/dwg/simplex.shx',
     '/dwg/hztxt.shx',
@@ -102,64 +123,138 @@
 
   // ==================== 表格 ====================
 
-  // 设备列表表格列
   const columns = [
-    { title: '#', key: 'index', width: 36 },
-    { title: '设备名称', dataIndex: 'strname', ellipsis: true },
-    { title: '类型', dataIndex: 'typeName', width: 56 },
-    { title: '状态', key: 'netStatus', width: 54 },
-    { title: '操作', key: 'action', width: 56 },
-    { title: '布点', key: 'markerStatus', width: 48 },
+    { title: '设备名称', dataIndex: 'strname', ellipsis: true, align: 'center' },
+    { title: '状态', key: 'netStatus', width: 60, align: 'center' },
+    { title: '操作', key: 'action', width: 60, align: 'center' },
   ];
-  // 设备列表数据
-  // const devices = ref<DeviceInfo[]>([
-  //   // { id: 'd001', name: '1#瓦斯传感器', type: '传感器', status: 'online', markerId: null },
-  //   // { id: 'd002', name: '2#瓦斯传感器', type: '传感器', status: 'online', markerId: null },
-  //   // { id: 'd003', name: '主通风机', type: '风机', status: 'online', markerId: null },
-  //   // { id: 'd004', name: '局部通风机', type: '风机', status: 'offline', markerId: null },
-  //   // { id: 'd005', name: '风门A', type: '风门', status: 'online', markerId: null },
-  //   // { id: 'd006', name: '风速传感器-1', type: '传感器', status: 'alarm', markerId: null },
-  //   // { id: 'd007', name: '温度传感器-1', type: '传感器', status: 'online', markerId: null },
-  //   // { id: 'd008', name: '风窗B', type: '风窗', status: 'online', markerId: null },
-  // ]);
-  // 状态颜色映射
+
   function statusColor(s: string) {
-    // return { online: 'green', offline: 'gray', alarm: 'red' }[s] || 'default';
-    return { 1: 'green', 0: 'gray', }[s] || 'default';
+    return { 1: 'green', 0: 'gray' }[s] || 'default';
   }
-  // 状态文本映射
+
   function statusText(s: string) {
-    // return { online: '在线', offline: '离线', alarm: '报警' }[s] || s;
-    return { 1: '在线', 0: '离线',  }[s] || s;
+    return { 1: '在线', 0: '离线' }[s] || s;
+  }
+
+  /** 获取设备唯一标识 */
+  function getDeviceId(device: DeviceRecord) {
+    return String(device.deviceID ?? device.id ?? '');
+  }
+
+  /** 已布点设备 ID 集合(保证列表按钮状态可响应更新) */
+  const placedDeviceIds = ref<Set<string>>(new Set());
+
+  /** 判断设备是否已完成布点 */
+  function isDevicePlaced(device: DeviceRecord) {
+    const id = getDeviceId(device);
+    return !!device.markerId || (id !== '' && placedDeviceIds.value.has(id));
+  }
+
+  /** 读取本地持久化的布点数据 */
+  function loadPersistedMarkers(): PersistedMarker[] {
+    try {
+      const raw = localStorage.getItem(MARKER_STORAGE_KEY);
+      if (!raw) return [];
+      const data = JSON.parse(raw);
+      return Array.isArray(data)
+        ? data.filter((item) => item && item.deviceId != null && typeof item.x === 'number' && typeof item.y === 'number')
+        : [];
+    } catch (e) {
+      console.warn('[布点] 读取本地布点失败:', e);
+      return [];
+    }
+  }
+
+  // 页面加载时先恢复「已布点」按钮状态(无需等待图纸加载)
+  placedDeviceIds.value = new Set(loadPersistedMarkers().map((m) => String(m.deviceId)));
+
+  /** 将当前 markersMap 写入本地存储 */
+  function savePersistedMarkers() {
+    const list: PersistedMarker[] = [];
+    markersMap.forEach((m) => {
+      list.push({ deviceId: m.deviceId, x: m.worldPos.x, y: m.worldPos.y });
+    });
+    localStorage.setItem(MARKER_STORAGE_KEY, JSON.stringify(list));
+  }
+
+  /** 将 markersMap 中的布点状态同步到设备列表(列表刷新后仍显示「已布点」) */
+  function syncPlacedStateToDevices() {
+    const next = new Set<string>();
+    if (markersMap.size > 0) {
+      markersMap.forEach((m, markerId) => {
+        next.add(m.deviceId);
+        const dev = findDevice(m.deviceId);
+        if (dev) dev.markerId = markerId;
+      });
+    } else {
+      loadPersistedMarkers().forEach((item) => next.add(String(item.deviceId)));
+    }
+    placedDeviceIds.value = next;
+    updateAllMarkerColors();
   }
-  // 模拟监测数据
-  function getMockMonitoring(deviceId: string) {
-    const d = devices.value.find((x) => x.id === deviceId);
+
+  /** 根据 deviceId 查找设备记录 */
+  function findDevice(deviceId: string) {
+    return deviceList.value.find((item) => getDeviceId(item as DeviceRecord) === deviceId) as DeviceRecord | undefined;
+  }
+
+  /** 判断设备是否处于报警 */
+  function isDeviceAlarm(device?: DeviceRecord | null) {
+    return !!device?.syswarnLevel;
+  }
+
+  /** 根据报警状态获取布点颜色 */
+  function getMarkerColor(device?: DeviceRecord | null) {
+    return isDeviceAlarm(device) ? MARKER_COLOR_ALARM : MARKER_COLOR_NORMAL;
+  }
+
+  /** 应用布点颜色(圆点背景 + 图标着色) */
+  function applyMarkerColor(
+    marker: { mesh: THREE.Mesh; iconSprite: THREE.Sprite },
+    color: number
+  ) {
+    (marker.mesh.material as THREE.MeshBasicMaterial).color.setHex(color);
+    (marker.iconSprite.material as THREE.SpriteMaterial).color.setHex(color);
+  }
+
+  /** 根据最新设备数据刷新所有已布点颜色 */
+  function updateAllMarkerColors() {
+    markersMap.forEach((m) => {
+      applyMarkerColor(m, getMarkerColor(findDevice(m.deviceId)));
+    });
+  }
+
+  /** 获取信息框展示内容:设备名称、安装位置、运行状态、报警状态、实时温度 */
+  function getMarkerInfoData(deviceId: string): MarkerInfoData {
+    const device = findDevice(deviceId);
+    const temperature = device?.readData?.temperature ?? device?.temperature;
     return {
-      deviceId,
-      deviceName: d?.name || '未知',
-      timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss'),
-      windSpeed: +(Math.random() * 5 + 1.5).toFixed(2),
-      gasConcentration: +(Math.random() * 0.5 + 0.15).toFixed(3),
-      temperature: +(Math.random() * 8 + 20).toFixed(1),
-      pressure: +(Math.random() * 100 + 900).toFixed(0),
-      dustDensity: +(Math.random() * 3 + 0.5).toFixed(2),
-      status: d?.status === 'online' ? '正常' : d?.status === 'alarm' ? '异常' : '离线',
+      deviceName: device?.strname || '未知',
+      installPos: device?.strinstallpos || '-',
+      runStatus: device?.netStatus == 1 ? '在线' : device?.netStatus == 0 ? '离线' : '-',
+      alarmStatus: device?.syswarnLevel ? '报警' : '正常',
+      temperature: temperature === undefined || temperature === null || temperature === '' ? '-' : String(temperature),
     };
   }
 
   // ==================== 状态 ====================
 
-  const viewerRef = ref<HTMLElement | null>(null);
+  /** 2D查看器是否已完成初始化 */
   const viewerReady = ref(false);
+  /** Viewer2d实例,用于操作CAD图纸 */
   let viewer: Viewer2d | null = null;
 
+  /** 右侧设备面板是否折叠 */
   const panelCollapsed = ref(true);
+  /** 是否处于布点模式(点击图纸可放置设备标记) */
   const placingMode = ref(false);
-  const placingDevice = ref<DeviceInfo | null>(null);
+  /** 当前待布点的设备对象 */
+  const placingDevice = ref<DeviceRecord | null>(null);
+  /** 当前鼠标在世界坐标系中的位置(用于坐标栏显示) */
   const currentCoord = reactive({ x: 0, y: 0, z: 0 });
 
-  // 存储 marker:markerId → { deviceId, worldPos, domEl }
+  // 存储 marker:markerId → { deviceId, worldPos, mesh, iconSprite }
   const markersMap = new Map<
     string,
     {
@@ -167,20 +262,13 @@
       worldPos: { x: number; y: number };
       mesh: THREE.Mesh;
       iconSprite: THREE.Sprite;
-      cssLabel: CSS2DObject;
     }
   >();
-  const allMeshes: THREE.Mesh[] = [];
-  const allIconSprites: THREE.Sprite[] = [];
-  const allCssLabels: CSS2DObject[] = [];
   const textureLoader = new THREE.TextureLoader();
 
-  const monitoringData = ref<ReturnType<typeof getMockMonitoring> | null>(null);
-  const modalVisible = ref(false);
-
   // ==================== 核心:坐标转换 ====================
 
-  // 参照 useEvent.ts: 画布容器有强制缩放 (widthScale/heightScale),需校准坐标
+  // 画布容器有强制缩放 (widthScale/heightScale),需校准坐标
   function scaledScreenCoord(e: { clientX: number; clientY: number }, container: HTMLElement): THREE.Vector2 {
     const appStore = useAppStoreWithOut();
     const ws = appStore.getWidthScale;
@@ -201,15 +289,20 @@
 
   // ==================== 放置模式 ====================
 
+  /** 布点模式下的悬浮圆点DOM元素(跟随鼠标移动) */
   let floatingDot: HTMLElement | null = null;
 
-  function enterPlacingMode(device: DeviceInfo) {
+  /**
+   * 进入布点模式
+   * 创建悬浮圆点,清除设备原有标记,设置布点状态
+   * @param device 待布点的设备记录
+   */
+  function enterPlacingMode(device: DeviceRecord) {
     if (!viewer) return;
     if (device.markerId) removeMarker(device.markerId);
     placingMode.value = true;
     placingDevice.value = device;
 
-    // 创建浮动跟随圆点
     if (!floatingDot) {
       floatingDot = document.createElement('div');
       floatingDot.id = 'floating-dot';
@@ -222,30 +315,34 @@
       if (cont) cont.appendChild(floatingDot);
     }
     if (floatingDot) floatingDot.style.display = '';
-
-    console.log(`[布点] 进入放置模式: ${device.name}`);
   }
-  // 退出放置模式
+
+  /**
+   * 退出布点模式
+   * 隐藏悬浮圆点,重置布点状态
+   */
   function exitPlacingMode() {
     placingMode.value = false;
     placingDevice.value = null;
     if (floatingDot) floatingDot.style.display = 'none';
   }
-  // 放置 marker
-  function placeMarker(device: DeviceInfo, location: { x: number; y: number }) {
+
+  function placeMarker(device: DeviceRecord, location: { x: number; y: number }, options?: { showInfo?: boolean; persist?: boolean }) {
     if (!viewer) return;
+    const showInfo = options?.showInfo !== false;
+    const persist = options?.persist !== false;
+
     markerIdCounter++;
     const markerId = `m${markerIdCounter}`;
 
-    // 动态半径:基于当前视口大小
     const cam = viewer.camera as THREE.OrthographicCamera;
     const frustumH = cam.top - cam.bottom;
-    const radius = frustumH * 0.008;
+    const markerColor = getMarkerColor(device);
 
-    // THREE.Mesh 实心圆点 — 绘制在场景中,随模型移动
+    // THREE.Mesh 实心圆点 — 作为报警状态背景色,并用于拾取点击
     const geom = new THREE.CircleGeometry(100, 32);
     const mat = new THREE.MeshBasicMaterial({
-      color: 0xff4d4f,
+      color: markerColor,
       side: THREE.DoubleSide,
       depthTest: false,
       depthWrite: false,
@@ -253,15 +350,21 @@
     const mesh = new THREE.Mesh(geom, mat);
     mesh.position.set(location.x, location.y, 0);
     mesh.renderOrder = 999;
-    mesh.matrixAutoUpdate = true; // 覆盖 scene.matrixAutoUpdate=false
-    mesh.updateMatrixWorld(); // 立即计算世界矩阵
-    mesh.userData = { markerId, deviceId: device.id };
+    mesh.matrixAutoUpdate = true;
+    mesh.updateMatrixWorld();
+    const deviceId = getDeviceId(device);
+    mesh.userData = { markerId, deviceId };
     viewer.scene.add(mesh);
-    allMeshes.push(mesh);
 
-    // 精灵图标(safetymonitor3D.png
+    // 精灵图标(按报警状态着色
     const tex = textureLoader.load('/texture/safetymonitor3D.png');
-    const spriteMat = new THREE.SpriteMaterial({ map: tex, depthTest: false, depthWrite: false, transparent: true });
+    const spriteMat = new THREE.SpriteMaterial({
+      map: tex,
+      color: markerColor,
+      depthTest: false,
+      depthWrite: false,
+      transparent: true,
+    });
     const icon = new THREE.Sprite(spriteMat);
     icon.position.set(location.x, location.y, 0);
     const iconSize = frustumH * 0.015;
@@ -270,70 +373,140 @@
     icon.matrixAutoUpdate = true;
     icon.updateMatrixWorld();
     viewer.scene.add(icon);
-    allIconSprites.push(icon);
-
-    // CSS2DObject 风速标签(图标上方)
-    const labelDiv = document.createElement('div');
-    labelDiv.textContent = `${+(Math.random() * 3 + 1.5).toFixed(1)} m/s`;
-    labelDiv.style.cssText =
-      'color:#00ffcc;font-size:22px;font-weight:bold;background:rgba(0,0,0,0.7);padding:2px 6px;border-radius:3px;white-space:nowrap;';
-    const cssLabel = new CSS2DObject(labelDiv);
-    cssLabel.position.set(location.x, location.y + 150, 0.1);
-    cssLabel.matrixAutoUpdate = true;
-    cssLabel.updateMatrixWorld();
-    viewer.scene.add(cssLabel);
-    allCssLabels.push(cssLabel);
 
     markersMap.set(markerId, {
-      deviceId: device.id,
+      deviceId,
       worldPos: { x: location.x, y: location.y },
       mesh,
       iconSprite: icon,
-      cssLabel,
     });
     device.markerId = markerId;
-    console.log(`[布点] 设备=${device.name} X=${location.x.toFixed(2)} Y=${location.y.toFixed(2)} r=${radius.toFixed(2)}`);
-    exitPlacingMode();
+    const next = new Set(placedDeviceIds.value);
+    next.add(deviceId);
+    placedDeviceIds.value = next;
+    if (persist) savePersistedMarkers();
+    if (showInfo) {
+      exitPlacingMode();
+      showMarkerInfo(markerId);
+    }
   }
-  // 移除 marker
+
+  /** 从本地存储恢复布点(刷新页面后还原) */
+  function restoreMarkers() {
+    if (!viewer) return;
+    const list = loadPersistedMarkers();
+    if (!list.length) return;
+    list.forEach((item) => {
+      const device = findDevice(item.deviceId) || ({ deviceID: item.deviceId, strname: '未知' } as DeviceRecord);
+      if (device.markerId && markersMap.has(device.markerId)) return;
+      if ([...markersMap.values()].some((m) => m.deviceId === item.deviceId)) return;
+      placeMarker(device, { x: item.x, y: item.y }, { showInfo: false, persist: false });
+    });
+    syncPlacedStateToDevices();
+  }
+
+  /**
+   * 移除指定标记点
+   * 从场景中移除mesh和图标精灵,释放THREE.js资源,更新状态并持久化
+   * @param markerId 标记点ID
+   */
   function removeMarker(markerId: string) {
     if (!viewer) return;
     const m = markersMap.get(markerId);
     if (!m) return;
-    const dev = devices.value.find((d) => d.markerId === markerId);
+    const dev = deviceList.value.find((d) => (d as DeviceRecord).markerId === markerId) as DeviceRecord | undefined;
     if (dev) dev.markerId = null;
+    if (m.deviceId) {
+      const next = new Set(placedDeviceIds.value);
+      next.delete(m.deviceId);
+      placedDeviceIds.value = next;
+    }
+    // 从场景移除并释放几何体和材质资源
     viewer.scene.remove(m.mesh);
     m.mesh.geometry.dispose();
     (m.mesh.material as THREE.Material).dispose();
     viewer.scene.remove(m.iconSprite);
     (m.iconSprite.material as THREE.SpriteMaterial).map?.dispose();
     m.iconSprite.material.dispose();
-    const ci = allCssLabels.indexOf(m.cssLabel);
-    if (ci > -1) allCssLabels.splice(ci, 1);
-
-    const idx = allMeshes.indexOf(m.mesh);
-    if (idx > -1) allMeshes.splice(idx, 1);
-    const si = allIconSprites.indexOf(m.iconSprite);
-    if (si > -1) allIconSprites.splice(si, 1);
     markersMap.delete(markerId);
+    savePersistedMarkers();
+  }
+
+  /** 撤销指定设备的布点 */
+  function revokePlacement(device: DeviceRecord) {
+    const deviceId = getDeviceId(device);
+    let markerId = device.markerId || null;
+    if (!markerId) {
+      for (const [id, m] of markersMap.entries()) {
+        if (m.deviceId === deviceId) {
+          markerId = id;
+          break;
+        }
+      }
+    }
+    if (!markerId) {
+      const next = new Set(placedDeviceIds.value);
+      next.delete(deviceId);
+      placedDeviceIds.value = next;
+      device.markerId = null;
+      savePersistedMarkers();
+      return;
+    }
+    hideInfoPanel();
+    removeMarker(markerId);
+    if (viewer) {
+      viewer.renderer.render(viewer.scene, viewer.camera);
+      viewer.getCssRender()?.render(viewer.scene, viewer.camera);
+    }
   }
 
   let infoPanel: HTMLElement | null = null;
-  // 显示 marker 信息面板
-   function showMarkerInfo(markerId: string) {
+  /** 当前打开的信息框对应的 markerId,用于缩放/平移时更新位置 */
+  let activeInfoMarkerId: string | null = null;
+  let infoPanelCloseHandler: ((e: MouseEvent) => void) | null = null;
+
+  function hideInfoPanel() {
+    infoPanel?.remove();
+    infoPanel = null;
+    activeInfoMarkerId = null;
+    if (infoPanelCloseHandler) {
+      document.removeEventListener('click', infoPanelCloseHandler);
+      infoPanelCloseHandler = null;
+    }
+  }
+
+  /** 根据 marker 世界坐标刷新信息框屏幕位置 */
+  function updateInfoPanelPosition() {
+    if (!infoPanel || !activeInfoMarkerId || !viewer) return;
+    const m = markersMap.get(activeInfoMarkerId);
+    if (!m) return;
+    const container = document.getElementById(containerId);
+    if (!container) return;
+    const sp = CoordinateUtils.world2Screen(new THREE.Vector3(m.worldPos.x, m.worldPos.y, 0), viewer.camera, container);
+    if (!sp) return;
+    infoPanel.style.left = `${sp.x + 16}px`;
+    infoPanel.style.top = `${sp.y - 10}px`;
+  }
+
+  /**
+   * 显示设备信息浮窗
+   * 在标记点附近创建信息面板,显示设备名称、安装位置、运行状态、报警状态、实时温度
+   * 点击面板外区域或关闭按钮可隐藏
+   * @param markerId 标记点ID
+   */
+  function showMarkerInfo(markerId: string) {
     const m = markersMap.get(markerId);
     if (!m || !viewer) return;
-    const data = getMockMonitoring(m.deviceId);
+    const info = getMarkerInfoData(m.deviceId);
     const container = document.getElementById(containerId);
     if (!container) return;
 
-    // 移除旧 panel
-    infoPanel?.remove();
+    hideInfoPanel();
 
-    // Marker 世界坐标 → 屏幕坐标
     const sp = CoordinateUtils.world2Screen(new THREE.Vector3(m.worldPos.x, m.worldPos.y, 0), viewer.camera, container);
 
     infoPanel = document.createElement('div');
+    activeInfoMarkerId = markerId;
     infoPanel.style.cssText = `
       position:absolute; left:${sp.x + 16}px; top:${sp.y - 10}px;
       background:rgba(0,0,0,0.88); color:#fff; font-size:12px;
@@ -343,37 +516,38 @@
     `;
     infoPanel.innerHTML = `
       <div style="display:flex;justify-content:space-between;margin-bottom:4px;">
-        <b>${data.deviceName}</b>
+        <b>${info.deviceName}</b>
         <span style="cursor:pointer;color:#aaa;margin-left:12px;" id="info-close">&times;</span>
       </div>
-      <div>风速: ${data.windSpeed} m/s</div>
-      <div>瓦斯浓度: ${data.gasConcentration}%</div>
-      <div>温度: ${data.temperature}°C</div>
-      <div>时间: ${data.timestamp}</div>
-      <div>状态: <span style="color:${data.status === '正常' ? '#52c41a' : '#ff4d4f'}">${data.status}</span></div>
+      <div>安装位置: ${info.installPos}</div>
+      <div>运行状态: <span style="color:${info.runStatus === '在线' ? '#52c41a' : info.runStatus === '离线' ? '#ff4d4f' : '#faad14'}">${info.runStatus}</span></div>
+      <div>报警状态: <span style="color:${info.alarmStatus === '报警' ? '#ff4d4f' : '#52c41a'}">${info.alarmStatus}</span></div>
+      <div>实时温度: ${info.temperature}${info.temperature !== '-' ? '℃' : ''}</div>
     `;
     container.appendChild(infoPanel);
-    infoPanel.querySelector('#info-close')?.addEventListener('click', () => infoPanel?.remove());
-    // 点击其他地方关闭
-    const closeHandler = (e: MouseEvent) => {
+    infoPanel.querySelector('#info-close')?.addEventListener('click', (e) => {
+      e.stopPropagation();
+      hideInfoPanel();
+    });
+    infoPanelCloseHandler = (e: MouseEvent) => {
       if (!infoPanel?.contains(e.target as Node)) {
-        infoPanel?.remove();
-        infoPanel = null;
-        document.removeEventListener('click', closeHandler);
+        hideInfoPanel();
       }
     };
-    setTimeout(() => document.addEventListener('click', closeHandler), 100);
-  }
-  // 点击 marker 事件
-  function onMarkerClick(markerId: string) {
-    console.log(`[标记点击] marker=${markerId}`);
-    showMarkerInfo(markerId);
+    setTimeout(() => {
+      if (infoPanelCloseHandler) document.addEventListener('click', infoPanelCloseHandler);
+    }, 100);
   }
 
   // ==================== 初始化 ====================
 
+  /**
+   * 组件挂载时初始化
+   * 1. 创建Viewer2d实例加载DWG图纸
+   * 2. 注册鼠标点击/移动事件(坐标显示、布点、信息查看)
+   * 3. 监听相机变化更新信息框位置
+   */
   onMounted(async () => {
-    // 初始化 viewer
     try {
       viewer = new Viewer2d({
         containerId,
@@ -382,26 +556,18 @@
         enableLayoutBar: true,
       });
       await viewer.setFont(fontFiles);
-      await viewer.loadModel({ modelId: 'main-plan', name: '总平面布置图', src: dwgSrc }, (event: ProgressEvent) => {
-        if (event.total > 0) console.log(`加载: ${Math.round((event.loaded / event.total) * 100)}%`);
-      });
+      await viewer.loadModel({ modelId: 'main-plan', name: '总平面布置图', src: dwgSrc });
 
       viewer.goToHomeView();
       viewerReady.value = true;
-      // initCss();
-
-      console.log('[DWG] Viewer2d 就绪');
 
-      const appStore = useAppStoreWithOut();
+      restoreMarkers();
+      viewer.renderer.render(viewer.scene, viewer.camera);
+      viewer.getCssRender()?.render(viewer.scene, viewer.camera);
 
-
-      // === 参照官方示例: ViewerEvent.MouseClick ===
       viewer.addEventListener(ViewerEvent.MouseClick, (data: any) => {
         if (!viewer) return;
 
-        
-
-        // 获取 world 坐标用于实时显示
         const container = document.getElementById(containerId);
         if (data?.event && container) {
           const sc = scaledScreenCoord(data.event, container);
@@ -413,17 +579,15 @@
           }
         }
 
-        // 检测是否点击了已有 marker mesh
         if (!placingMode.value && data?.event && container) {
           const sc2 = scaledScreenCoord(data.event, container);
           const hit = viewer.pickObject(sc2);
           if (hit?.object?.userData?.markerId) {
-            onMarkerClick(hit.object.userData.markerId as string);
+            showMarkerInfo(hit.object.userData.markerId as string);
             return;
           }
         }
 
-        // 点击空白背景 → 放置 marker
         if (!data?.entityData && !data?.markupData && !data?.measureData) {
           if (placingMode.value && placingDevice.value) {
             const location = getHitResult(data.event);
@@ -432,129 +596,85 @@
         }
 
         viewer.renderer.render(viewer.scene, viewer.camera);
-        viewer.getCssRender().render(viewer.scene, viewer.camera);
-        // css3DRenderer?.render(viewer.scene, viewer.camera);
+        viewer.getCssRender()?.render(viewer.scene, viewer.camera);
       });
 
-      // === 鼠标移动:实时更新坐标 + 浮动圆点跟随 ===
       const container = document.getElementById(containerId);
       if (container) {
         container.addEventListener('mousemove', (e: MouseEvent) => {
           if (!viewer) return;
-          // const ws = appStore.getWidthScale;
-          // const hs = appStore.getHeightScale;
-          // 浮动圆点跟随鼠标(缩放校准后坐标,与 marker 放置位置一致)
           const sc = scaledScreenCoord(e, container);
           if (floatingDot && placingMode.value) {
             floatingDot.style.left = sc.x + 'px';
-            floatingDot.style.top = sc.y  + 'px';
+            floatingDot.style.top = sc.y + 'px';
           }
-          // 世界坐标
           const wp = CoordinateUtils.screen2World(sc, viewer.camera, container);
           if (wp) {
             currentCoord.x = wp.x;
             currentCoord.y = wp.y;
             currentCoord.z = wp.z;
           }
-
-          // css3DRenderer?.render(viewer.scene, viewer.camera);
         });
       }
 
-      // === Mesh 标记在场景坐标系中,随模型自动移动,无需 CameraChange 更新 ===
+      // 滚轮缩放 / 平移时,信息悬浮框跟随布点
+      viewer.addEventListener(ViewerEvent.CameraChange, () => {
+        updateInfoPanelPosition();
+      });
+      viewer.addEventListener(ViewerEvent.AfterRender, () => {
+        if (infoPanel && activeInfoMarkerId) updateInfoPanelPosition();
+      });
     } catch (err) {
       console.error('[DWG] 初始化失败:', err);
     }
   });
-  /** 键盘事件处理:Esc 键退出放置模式 */
+
+  /**
+   * 监听设备列表变化
+   * 当外部传入的设备数据更新时,同步布点状态到列表(保证按钮状态正确)
+   */
+  watch(
+    deviceList,
+    () => {
+      if (!viewerReady.value) return;
+      syncPlacedStateToDevices();
+    },
+    { deep: true }
+  );
+
+  /** 键盘事件处理:Esc键退出布点模式 */
   const onKeydown = (e: KeyboardEvent) => {
     if (e.key === 'Escape' && placingMode.value) exitPlacingMode();
   };
-  // 注册键盘事件监听
-   onMounted(() => window.addEventListener('keydown', onKeydown));
+  onMounted(() => window.addEventListener('keydown', onKeydown));
 
-  /** 组件卸载时清理:移除事件监听、销毁所有 marker、释放 viewer 资源 */
+  /**
+   * 组件卸载时清理资源
+   * 移除事件监听、销毁Viewer实例、释放所有THREE.js资源
+   */
   onUnmounted(() => {
     window.removeEventListener('keydown', onKeydown);
+    hideInfoPanel();
     floatingDot?.remove();
     floatingDot = null;
     if (viewer) {
-      allMeshes.forEach((m) => {
-        viewer!.scene.remove(m);
-        m.geometry.dispose();
-        (m.material as THREE.Material).dispose();
-      });
-      allMeshes.length = 0;
-      allIconSprites.forEach((s) => {
-        viewer!.scene.remove(s);
-        (s.material as THREE.SpriteMaterial).map?.dispose();
-        s.material.dispose();
+      markersMap.forEach((m) => {
+        viewer!.scene.remove(m.mesh);
+        m.mesh.geometry.dispose();
+        (m.mesh.material as THREE.Material).dispose();
+        viewer!.scene.remove(m.iconSprite);
+        (m.iconSprite.material as THREE.SpriteMaterial).map?.dispose();
+        m.iconSprite.material.dispose();
       });
-      allIconSprites.length = 0;
-      allCssLabels.forEach((l) => viewer!.scene?.remove(l));
-      allCssLabels.length = 0;
-    }
-    markersMap.clear();
-    if (viewer) {
+      markersMap.clear();
       viewer.destroy?.();
       viewer = null;
     }
   });
 </script>
 
-<style lang="less">
-  /* 参照官方示例 hotpoint 样式 */
-  .hotpoint {
-    opacity: 0.85;
-    pointer-events: auto;
-  }
-  .hotpoint-dot {
-    width: 16px;
-    height: 16px;
-    cursor: pointer;
-    background: #ff4d4f;
-    border: 2px solid #fff;
-    border-radius: 50%;
-    box-shadow: 0 0 10px rgba(255, 77, 79, 0.8);
-  }
-  .hotpoint-dot:hover {
-    box-shadow: 0 0 16px rgba(255, 100, 100, 0.9);
-    background: #ff6666;
-  }
-  .hotpoint-panel {
-    position: absolute;
-    top: 20px;
-    left: -75px;
-    min-width: 120px;
-    background: rgba(0, 0, 0, 0.85);
-    border: 1px solid rgba(255, 255, 255, 0.2);
-    border-radius: 6px;
-    color: #fff;
-    font-size: 12px;
-  }
-  .hotpoint-close {
-    text-align: right;
-    padding: 2px 6px;
-    cursor: pointer;
-    color: #aaa;
-    font-size: 14px;
-    &:hover {
-      color: #fff;
-    }
-  }
-  .hotpoint-body {
-    padding: 4px 8px 8px;
-    min-height: 30px;
-    white-space: nowrap;
-  }
-  .hide {
-    display: none;
-  }
-</style>
-
 <style lang="less" scoped>
-  @import '/@/design/theme.less';
-
+  /* 最外层容器:占满父级、隐藏溢出 */
   .content {
     width: 100%;
     height: 100%;
@@ -562,17 +682,20 @@
     overflow: hidden;
   }
 
+  /* 视图区域:canvas 的父容器 */
   .viewer-area {
     width: 100%;
     height: 100%;
     position: relative;
   }
+  /* 图纸渲染 canvas 容器 */
   .viewer-canvas {
     width: 100%;
     height: 100%;
     position: relative;
   }
 
+  /* 底部坐标栏:显示当前鼠标在世界坐标中的位置 */
   .coord-bar {
     position: absolute;
     bottom: 8px;
@@ -585,36 +708,43 @@
     font-size: 13px;
     font-family: Consolas, Monaco, monospace;
     z-index: 200;
-    pointer-events: none;
+    pointer-events: none; /* 不阻挡鼠标事件,让事件穿透到 canvas */
     white-space: nowrap;
     > span {
       margin: 0 4px;
     }
     .placing-hint {
-      color: #faad14;
+      color: #faad14; /* 放置模式下的提示文字颜色 */
     }
   }
 
+  /* 右侧设备列表面板(毛玻璃效果) */
   .device-panel {
     position: absolute;
     top: 8px;
     right: 8px;
     width: 300px;
     z-index: 300;
-    background: var(--vent-device-manager-box-bg, rgba(20, 20, 40, 0.94));
-    border: 1px solid var(--vent-device-manager-box-border, rgba(255, 255, 255, 0.15));
-    border-radius: 8px;
+    background: rgba(16, 28, 48, 0.45);
+    border: 1px solid rgba(255, 255, 255, 0.18);
+    border-radius: 10px;
     overflow: hidden;
+    backdrop-filter: blur(14px) saturate(1.35);
+    -webkit-backdrop-filter: blur(14px) saturate(1.35);
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
+    /* 折叠状态:宽度自适应 */
     &.collapsed {
       width: auto;
     }
+    /* 面板头部:标题 + 展开/收起按钮 */
     .panel-header {
       display: flex;
       justify-content: space-between;
       align-items: center;
       padding: 8px 12px;
       cursor: pointer;
-      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+      background: rgba(255, 255, 255, 0.04);
+      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
       .panel-title {
         font-size: 14px;
         font-weight: 600;
@@ -625,25 +755,26 @@
         color: rgba(255, 255, 255, 0.5);
       }
     }
+    /* 面板内容区:设备表格 */
     .panel-body {
       padding: 6px;
-    }
-  }
+      background: transparent;
 
-  .monitor-data {
-    .monitor-row {
-      display: flex;
-      padding: 8px 0;
-      border-bottom: 1px solid #f0f0f0;
-      font-size: 14px;
-    }
-    .monitor-row:last-child {
-      border-bottom: none;
-    }
-    .monitor-key {
-      width: 72px;
-      color: #888;
-      flex-shrink: 0;
+      :deep(.ant-table) {
+        background: transparent !important;
+      }
+      :deep(.ant-table-thead > tr > th) {
+        background: rgba(255, 255, 255, 0.06) !important;
+        border-bottom-color: rgba(255, 255, 255, 0.1) !important;
+        color: rgba(255, 255, 255, 0.85);
+      }
+      :deep(.ant-table-tbody > tr > td) {
+        background: transparent !important;
+        border-bottom-color: rgba(255, 255, 255, 0.06) !important;
+      }
+      :deep(.ant-table-tbody > tr:hover > td) {
+        background: rgba(255, 255, 255, 0.08) !important;
+      }
     }
   }
 </style>

+ 9 - 1
src/views/vent/deviceManager/comment/warningTabel/warning.data.ts

@@ -295,7 +295,15 @@ export const workFaceWarningFormSchemas: FormSchema[] = [
     label: '所属系统',
     field: 'systemType',
     component: 'JDictSelectTag',
-    componentProps: { dictCode: 'kindtype' },
+    //componentProps: { dictCode: 'kindtype' },
+    componentProps:{
+      options: [
+        {
+          label: '防灭火系统',
+          value: 'fireS',
+        },
+      ],
+    }
   },
 ];