lxh 3 дней назад
Родитель
Сommit
88a8289f30

+ 283 - 173
src/views/vent/cad/dwgViewer.vue

@@ -1,57 +1,41 @@
 <template>
-  <!-- 主内容容器 -->
   <div class="content">
-    <!-- CAD图纸查看区域 -->
     <div class="viewer-area">
-      <!-- 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>
-        <!-- 布点模式下显示提示信息,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 }">
-            <!-- 状态列:根据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
-                v-if="!isDevicePlaced(record)"
-                type="primary"
-                size="small"
-                :disabled="placingMode"
-                @click="enterPlacingMode(record)"
-              >布点</a-button> -->
-               <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>
+              <a-button
+                v-else
+                type="link"
+                size="small"
+                danger
+                :disabled="placingMode"
+                @click="revokePlacement(record)"
+              >撤销</a-button>
             </template>
           </template>
         </a-table>
@@ -106,20 +90,33 @@
 
   const containerId = 'dwg-viewer-canvas';
   const dwgSrc = '/dwg/2026年2月总平面布置图_无高程.dwg';
+  const MARKER_ICON_SRC = '/texture/safetymonitor3D.png';
   /** 布点持久化存储 key(按图纸区分) */
   const MARKER_STORAGE_KEY = 'dwg-viewer-markers:main-plan';
   /** 正常状态布点颜色(绿色) */
   const MARKER_COLOR_NORMAL = 0x52c41a;
   /** 报警状态布点颜色(红色) */
   const MARKER_COLOR_ALARM = 0xff4d4f;
+  /** 布点圆点几何半径(世界坐标) */
+  const MARKER_MESH_RADIUS = 100;
+  /** 信息框相对布点右边缘的间距(px) */
+  const INFO_PANEL_GAP = 15;
+  /** 信息框字号基准:布点屏幕半宽为该值时,字号为 12px */
+  const INFO_PANEL_BASE_HALF = 36;
+  /** 信息框字号基准(px) */
+  const INFO_PANEL_BASE_FONT = 12;
+  /**
+   * 初始化仅加载必要字体,减少 setFont 阻塞时间
+   * (图纸中文依赖 shx + 一份 TrueType/typeface)
+   */
   const fontFiles = [
     '/dwg/simplex.shx',
     '/dwg/hztxt.shx',
-    '/dwg/arial.ttf',
-    '/dwg/Microsoft_YaHei.ttf',
     '/dwg/Microsoft_YaHei_Regular.typeface.json',
   ];
   let markerIdCounter = 0;
+  /** 共享圆点几何体,避免每个布点重复创建 */
+  let sharedCircleGeom: THREE.CircleGeometry | null = null;
 
   // ==================== 表格 ====================
 
@@ -210,10 +207,7 @@
   }
 
   /** 应用布点颜色(圆点背景 + 图标着色) */
-  function applyMarkerColor(
-    marker: { mesh: THREE.Mesh; iconSprite: THREE.Sprite },
-    color: number
-  ) {
+  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);
   }
@@ -225,7 +219,7 @@
     });
   }
 
-  /** 获取信息框展示内容:设备名称、安装位置、运行状态、报警状态、实时温度 */
+  /** 获取信息框展示内容 */
   function getMarkerInfoData(deviceId: string): MarkerInfoData {
     const device = findDevice(deviceId);
     const temperature = device?.readData?.temperature ?? device?.temperature;
@@ -233,28 +227,20 @@
       deviceName: device?.strname || '未知',
       installPos: device?.strinstallpos || '-',
       runStatus: device?.netStatus == 1 ? '在线' : device?.netStatus == 0 ? '离线' : '-',
-      alarmStatus: device?.syswarnLevel ? '报警' : '正常',
+      alarmStatus: isDeviceAlarm(device) ? '报警' : '正常',
       temperature: temperature === undefined || temperature === null || temperature === '' ? '-' : String(temperature),
     };
   }
 
   // ==================== 状态 ====================
 
-  /** 2D查看器是否已完成初始化 */
   const viewerReady = ref(false);
-  /** Viewer2d实例,用于操作CAD图纸 */
   let viewer: Viewer2d | null = null;
-
-  /** 右侧设备面板是否折叠 */
   const panelCollapsed = ref(true);
-  /** 是否处于布点模式(点击图纸可放置设备标记) */
   const placingMode = ref(false);
-  /** 当前待布点的设备对象 */
   const placingDevice = ref<DeviceRecord | null>(null);
-  /** 当前鼠标在世界坐标系中的位置(用于坐标栏显示) */
   const currentCoord = reactive({ x: 0, y: 0, z: 0 });
 
-  // 存储 marker:markerId → { deviceId, worldPos, mesh, iconSprite }
   const markersMap = new Map<
     string,
     {
@@ -265,16 +251,47 @@
     }
   >();
   const textureLoader = new THREE.TextureLoader();
+  /** 布点精灵图标共享纹理(预加载后复用) */
+  let markerIconTexture: THREE.Texture | null = null;
+  /** 信息框跟随事件是否已挂载 */
+  let infoPanelFollowBound = false;
+  /** mousemove 坐标更新 rAF */
+  let mouseMoveRaf = 0;
+
+  function getSharedCircleGeom() {
+    if (!sharedCircleGeom) {
+      // 分段数降至 24,足够圆滑且创建更快
+      sharedCircleGeom = new THREE.CircleGeometry(MARKER_MESH_RADIUS, 24);
+    }
+    return sharedCircleGeom;
+  }
+
+  /** 获取/预加载布点图标纹理 */
+  function ensureMarkerIconTexture() {
+    if (markerIconTexture) return markerIconTexture;
+    markerIconTexture = textureLoader.load(MARKER_ICON_SRC, () => {
+      if (viewer && markersMap.size > 0) forceViewerRender();
+    });
+    return markerIconTexture;
+  }
+
+  function forceViewerRender() {
+    if (!viewer) return;
+    viewer.renderer.render(viewer.scene, viewer.camera);
+    viewer.getCssRender()?.render(viewer.scene, viewer.camera);
+  }
+
+  /** 缓存 appStore,避免每次坐标换算重复取 store */
+  const appStore = useAppStoreWithOut();
 
   // ==================== 核心:坐标转换 ====================
 
-  // 画布容器有强制缩放 (widthScale/heightScale),需校准坐标
   function scaledScreenCoord(e: { clientX: number; clientY: number }, container: HTMLElement): THREE.Vector2 {
-    const appStore = useAppStoreWithOut();
-    const ws = appStore.getWidthScale;
-    const hs = appStore.getHeightScale;
     const rect = container.getBoundingClientRect();
-    return new THREE.Vector2((e.clientX - rect.left) / ws, (e.clientY - rect.top) / hs);
+    return new THREE.Vector2(
+      (e.clientX - rect.left) / appStore.getWidthScale,
+      (e.clientY - rect.top) / appStore.getHeightScale
+    );
   }
 
   function getHitResult(event: MouseEvent): { x: number; y: number } | null {
@@ -287,16 +304,22 @@
     return { x: worldPos.x, y: worldPos.y };
   }
 
+  function updateCurrentCoord(event: MouseEvent, container: HTMLElement) {
+    if (!viewer) return;
+    const sc = scaledScreenCoord(event, container);
+    const wp = CoordinateUtils.screen2World(sc, viewer.camera, container);
+    if (wp) {
+      currentCoord.x = wp.x;
+      currentCoord.y = wp.y;
+      currentCoord.z = wp.z;
+    }
+    return sc;
+  }
+
   // ==================== 放置模式 ====================
 
-  /** 布点模式下的悬浮圆点DOM元素(跟随鼠标移动) */
   let floatingDot: HTMLElement | null = null;
 
-  /**
-   * 进入布点模式
-   * 创建悬浮圆点,清除设备原有标记,设置布点状态
-   * @param device 待布点的设备记录
-   */
   function enterPlacingMode(device: DeviceRecord) {
     if (!viewer) return;
     if (device.markerId) removeMarker(device.markerId);
@@ -314,23 +337,24 @@
       const cont = document.getElementById(containerId);
       if (cont) cont.appendChild(floatingDot);
     }
-    if (floatingDot) floatingDot.style.display = '';
+    floatingDot.style.display = '';
   }
 
-  /**
-   * 退出布点模式
-   * 隐藏悬浮圆点,重置布点状态
-   */
   function exitPlacingMode() {
     placingMode.value = false;
     placingDevice.value = null;
     if (floatingDot) floatingDot.style.display = 'none';
   }
 
-  function placeMarker(device: DeviceRecord, location: { x: number; y: number }, options?: { showInfo?: boolean; persist?: boolean }) {
+  function placeMarker(
+    device: DeviceRecord,
+    location: { x: number; y: number },
+    options?: { showInfo?: boolean; persist?: boolean; skipRender?: boolean }
+  ) {
     if (!viewer) return;
     const showInfo = options?.showInfo !== false;
     const persist = options?.persist !== false;
+    const skipRender = !!options?.skipRender;
 
     markerIdCounter++;
     const markerId = `m${markerIdCounter}`;
@@ -339,15 +363,14 @@
     const frustumH = cam.top - cam.bottom;
     const markerColor = getMarkerColor(device);
 
-    // THREE.Mesh 实心圆点 — 作为报警状态背景色,并用于拾取点击
-    const geom = new THREE.CircleGeometry(100, 32);
+    // THREE.Mesh 实心圆点 — 共享几何体,加快批量恢复
     const mat = new THREE.MeshBasicMaterial({
       color: markerColor,
       side: THREE.DoubleSide,
       depthTest: false,
       depthWrite: false,
     });
-    const mesh = new THREE.Mesh(geom, mat);
+    const mesh = new THREE.Mesh(getSharedCircleGeom(), mat);
     mesh.position.set(location.x, location.y, 0);
     mesh.renderOrder = 999;
     mesh.matrixAutoUpdate = true;
@@ -356,10 +379,9 @@
     mesh.userData = { markerId, deviceId };
     viewer.scene.add(mesh);
 
-    // 精灵图标(按报警状态着色)
-    const tex = textureLoader.load('/texture/safetymonitor3D.png');
+    // 精灵图标(使用预加载纹理)
     const spriteMat = new THREE.SpriteMaterial({
-      map: tex,
+      map: ensureMarkerIconTexture(),
       color: markerColor,
       depthTest: false,
       depthWrite: false,
@@ -372,6 +394,7 @@
     icon.renderOrder = 1000;
     icon.matrixAutoUpdate = true;
     icon.updateMatrixWorld();
+    icon.userData = { markerId, deviceId };
     viewer.scene.add(icon);
 
     markersMap.set(markerId, {
@@ -384,6 +407,7 @@
     const next = new Set(placedDeviceIds.value);
     next.add(deviceId);
     placedDeviceIds.value = next;
+    if (!skipRender) forceViewerRender();
     if (persist) savePersistedMarkers();
     if (showInfo) {
       exitPlacingMode();
@@ -391,42 +415,39 @@
     }
   }
 
-  /** 从本地存储恢复布点(刷新页面后还原) */
+  /** 从本地存储恢复布点(批量创建,最后统一渲染) */
   function restoreMarkers() {
     if (!viewer) return;
     const list = loadPersistedMarkers();
     if (!list.length) return;
+    const placedIds = new Set([...markersMap.values()].map((m) => m.deviceId));
     list.forEach((item) => {
+      if (placedIds.has(item.deviceId)) return;
       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 });
+      placeMarker(device, { x: item.x, y: item.y }, { showInfo: false, persist: false, skipRender: true });
+      placedIds.add(item.deviceId);
     });
     syncPlacedStateToDevices();
+    forceViewerRender();
   }
 
-  /**
-   * 移除指定标记点
-   * 从场景中移除mesh和图标精灵,释放THREE.js资源,更新状态并持久化
-   * @param markerId 标记点ID
-   */
   function removeMarker(markerId: string) {
     if (!viewer) return;
     const m = markersMap.get(markerId);
     if (!m) return;
-    const dev = deviceList.value.find((d) => (d as DeviceRecord).markerId === markerId) as DeviceRecord | undefined;
-    if (dev) dev.markerId = null;
+    const dev = findDevice(m.deviceId);
+    if (dev && dev.markerId === markerId) 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();
+    // 共享几何体不 dispose
     (m.mesh.material as THREE.Material).dispose();
     viewer.scene.remove(m.iconSprite);
-    (m.iconSprite.material as THREE.SpriteMaterial).map?.dispose();
+    // 共享纹理不 dispose,只释放材质
     m.iconSprite.material.dispose();
     markersMap.delete(markerId);
     savePersistedMarkers();
@@ -464,6 +485,8 @@
   /** 当前打开的信息框对应的 markerId,用于缩放/平移时更新位置 */
   let activeInfoMarkerId: string | null = null;
   let infoPanelCloseHandler: ((e: MouseEvent) => void) | null = null;
+  /** 信息框跟随更新的 rAF 标记,避免同一帧重复计算 */
+  let infoPanelFollowRaf = 0;
 
   function hideInfoPanel() {
     infoPanel?.remove();
@@ -473,27 +496,139 @@
       document.removeEventListener('click', infoPanelCloseHandler);
       infoPanelCloseHandler = null;
     }
+    if (infoPanelFollowRaf) {
+      cancelAnimationFrame(infoPanelFollowRaf);
+      infoPanelFollowRaf = 0;
+    }
+    unbindInfoPanelFollow();
+  }
+
+  /** 仅在信息框打开时绑定跟随事件,降低空闲时开销 */
+  function bindInfoPanelFollow() {
+    if (!viewer || infoPanelFollowBound) return;
+    viewer.addEventListener(ViewerEvent.CameraChange, scheduleInfoPanelFollow);
+    viewer.addEventListener(ViewerEvent.ControlChange, scheduleInfoPanelFollow);
+    viewer.addEventListener(ViewerEvent.AfterRender, scheduleInfoPanelFollow);
+    const container = document.getElementById(containerId);
+    container?.addEventListener('wheel', scheduleInfoPanelFollow, { passive: true });
+    infoPanelFollowBound = true;
+  }
+
+  function unbindInfoPanelFollow() {
+    if (!viewer || !infoPanelFollowBound) return;
+    viewer.removeEventListener(ViewerEvent.CameraChange, scheduleInfoPanelFollow);
+    viewer.removeEventListener(ViewerEvent.ControlChange, scheduleInfoPanelFollow);
+    viewer.removeEventListener(ViewerEvent.AfterRender, scheduleInfoPanelFollow);
+    const container = document.getElementById(containerId);
+    container?.removeEventListener('wheel', scheduleInfoPanelFollow);
+    infoPanelFollowBound = false;
   }
 
-  /** 根据 marker 世界坐标刷新信息框屏幕位置 */
-  function updateInfoPanelPosition() {
-    if (!infoPanel || !activeInfoMarkerId || !viewer) return;
+  /** 将世界坐标转为容器内屏幕坐标 */
+  function worldToContainerScreen(x: number, y: number) {
+    if (!viewer) return null;
+    const container = document.getElementById(containerId);
+    if (!container) return null;
+    return CoordinateUtils.world2Screen(new THREE.Vector3(x, y, 0), viewer.camera, container);
+  }
+
+  /**
+   * 计算布点在屏幕上的半宽(取圆点与图标较大值)
+   * 随滚轮缩放变化,用于信息框避让与字号缩放
+   */
+  function getMarkerScreenHalfSize(marker: { worldPos: { x: number; y: number }; iconSprite: THREE.Sprite }) {
+    const { x, y } = marker.worldPos;
+    const iconHalf = Math.max(marker.iconSprite.scale.x, marker.iconSprite.scale.y) / 2;
+    const worldHalf = Math.max(MARKER_MESH_RADIUS, iconHalf);
+    const center = worldToContainerScreen(x, y);
+    const edge = worldToContainerScreen(x + worldHalf, y);
+    if (!center || !edge) return 12;
+    return Math.max(Math.abs(edge.x - center.x), 8);
+  }
+
+  /**
+   * 按布点当前屏幕尺寸,更新信息框位置与字号
+   * - 位置:始终在布点右边缘外侧 15px,垂直居中
+   * - 字号/内边距:随布点放大缩小同步变化
+   */
+  function applyInfoPanelPosition() {
+    if (!infoPanel || !activeInfoMarkerId) 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);
+    const sp = worldToContainerScreen(m.worldPos.x, m.worldPos.y);
     if (!sp) return;
-    infoPanel.style.left = `${sp.x + 16}px`;
-    infoPanel.style.top = `${sp.y - 10}px`;
+
+    const half = getMarkerScreenHalfSize(m);
+    const left = sp.x + half + INFO_PANEL_GAP;
+    const top = sp.y;
+
+    // 字号随布点屏幕尺寸缩放,限制在合理范围,避免过小/过大
+    const scale = Math.min(2.2, Math.max(0.7, half / INFO_PANEL_BASE_HALF));
+    const fontSize = +(INFO_PANEL_BASE_FONT * scale).toFixed(1);
+    const padY = +(6 * scale).toFixed(1);
+    const padX = +(8 * scale).toFixed(1);
+
+    infoPanel.style.left = `${left}px`;
+    infoPanel.style.top = `${top}px`;
+    infoPanel.style.transform = 'translateY(-50%)';
+    infoPanel.style.fontSize = `${fontSize}px`;
+    infoPanel.style.padding = `${padY}px ${padX}px`;
+    infoPanel.style.borderRadius = `${Math.max(4, 6 * scale)}px`;
+  }
+
+  /** 调度信息框跟随更新 */
+  function scheduleInfoPanelFollow() {
+    if (!infoPanel || !activeInfoMarkerId) return;
+    if (infoPanelFollowRaf) return;
+    infoPanelFollowRaf = requestAnimationFrame(() => {
+      infoPanelFollowRaf = 0;
+      applyInfoPanelPosition();
+    });
+  }
+
+  /** 从拾取对象或其父级上读取 markerId */
+  function getMarkerIdFromObject(object: any): string | null {
+    let obj = object;
+    while (obj) {
+      if (obj.userData?.markerId) return obj.userData.markerId as string;
+      obj = obj.parent;
+    }
+    return null;
   }
 
   /**
-   * 显示设备信息浮窗
-   * 在标记点附近创建信息面板,显示设备名称、安装位置、运行状态、报警状态、实时温度
-   * 点击面板外区域或关闭按钮可隐藏
-   * @param markerId 标记点ID
+   * 根据屏幕坐标查找被点击的布点
+   * 优先使用 pickObject,失败时按与布点中心的屏幕距离判定(保证点中图标也能弹出)
    */
+  function findClickedMarkerId(sc: THREE.Vector2): string | null {
+    if (!viewer) return null;
+
+    const hit = viewer.pickObject(sc);
+    const pickedId = getMarkerIdFromObject(hit?.object);
+    if (pickedId && markersMap.has(pickedId)) return pickedId;
+
+    const container = document.getElementById(containerId);
+    if (!container) return null;
+
+    let bestId: string | null = null;
+    let bestDist = Infinity;
+    markersMap.forEach((m, id) => {
+      const sp = worldToContainerScreen(m.worldPos.x, m.worldPos.y);
+      if (!sp) return;
+      const dx = sp.x - sc.x;
+      const dy = sp.y - sc.y;
+      const dist = Math.sqrt(dx * dx + dy * dy);
+      // 点击半径:布点屏幕半宽,至少 24px,便于点选
+      const hitRadius = Math.max(getMarkerScreenHalfSize(m), 24);
+      if (dist <= hitRadius && dist < bestDist) {
+        bestDist = dist;
+        bestId = id;
+      }
+    });
+    return bestId;
+  }
+
+  /** 显示设备信息浮窗(布点右侧 15px,尺寸随缩放自适应) */
   function showMarkerInfo(markerId: string) {
     const m = markersMap.get(markerId);
     if (!m || !viewer) return;
@@ -503,15 +638,14 @@
 
     hideInfoPanel();
 
-    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;
+      position:absolute; left:0; top:0;
+      transform:translateY(-50%);
       background:rgba(0,0,0,0.88); color:#fff; font-size:12px;
       border:1px solid rgba(255,255,255,0.2); border-radius:6px;
-      padding:8px 10px; white-space:nowrap; z-index:600;
+      padding:6px 8px; white-space:nowrap; z-index:600;
       pointer-events:auto; line-height:1.8;
     `;
     infoPanel.innerHTML = `
@@ -525,65 +659,69 @@
       <div>实时温度: ${info.temperature}${info.temperature !== '-' ? '℃' : ''}</div>
     `;
     container.appendChild(infoPanel);
+    bindInfoPanelFollow();
+    applyInfoPanelPosition();
     infoPanel.querySelector('#info-close')?.addEventListener('click', (e) => {
       e.stopPropagation();
       hideInfoPanel();
     });
     infoPanelCloseHandler = (e: MouseEvent) => {
-      if (!infoPanel?.contains(e.target as Node)) {
-        hideInfoPanel();
-      }
+      if (!infoPanel?.contains(e.target as Node)) hideInfoPanel();
     };
     setTimeout(() => {
       if (infoPanelCloseHandler) document.addEventListener('click', infoPanelCloseHandler);
     }, 100);
   }
 
+  /** 首屏绘制完成后再恢复历史布点,避免阻塞图纸首次可见 */
+  function scheduleRestoreMarkers() {
+    const run = () => restoreMarkers();
+    if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
+      (window as any).requestIdleCallback(run, { timeout: 800 });
+    } else {
+      requestAnimationFrame(() => setTimeout(run, 0));
+    }
+  }
+
   // ==================== 初始化 ====================
 
-  /**
-   * 组件挂载时初始化
-   * 1. 创建Viewer2d实例加载DWG图纸
-   * 2. 注册鼠标点击/移动事件(坐标显示、布点、信息查看)
-   * 3. 监听相机变化更新信息框位置
-   */
+  const onKeydown = (e: KeyboardEvent) => {
+    if (e.key === 'Escape' && placingMode.value) exitPlacingMode();
+  };
+
   onMounted(async () => {
+    window.addEventListener('keydown', onKeydown);
+    // 与 Viewer 初始化并行预加载图标,缩短首次布点等待
+    ensureMarkerIconTexture();
+
     try {
       viewer = new Viewer2d({
         containerId,
         enableSpinner: true,
         enableProgressBar: true,
-        enableLayoutBar: true,
+        // 本页不需要布局切换栏,关闭可减少初始化与 UI 开销
+        enableLayoutBar: false,
       });
       await viewer.setFont(fontFiles);
       await viewer.loadModel({ modelId: 'main-plan', name: '总平面布置图', src: dwgSrc });
 
       viewer.goToHomeView();
       viewerReady.value = true;
+      forceViewerRender();
 
-      restoreMarkers();
-      viewer.renderer.render(viewer.scene, viewer.camera);
-      viewer.getCssRender()?.render(viewer.scene, viewer.camera);
-
+      // 事件尽早绑定,保证可交互;历史布点延后恢复
       viewer.addEventListener(ViewerEvent.MouseClick, (data: any) => {
         if (!viewer) return;
-
         const container = document.getElementById(containerId);
-        if (data?.event && container) {
-          const sc = scaledScreenCoord(data.event, container);
-          const wp = CoordinateUtils.screen2World(sc, viewer.camera, container);
-          if (wp) {
-            currentCoord.x = wp.x;
-            currentCoord.y = wp.y;
-            currentCoord.z = wp.z;
-          }
-        }
+        if (!data?.event || !container) return;
+
+        const sc = updateCurrentCoord(data.event, container);
 
-        if (!placingMode.value && data?.event && container) {
-          const sc2 = scaledScreenCoord(data.event, container);
-          const hit = viewer.pickObject(sc2);
-          if (hit?.object?.userData?.markerId) {
-            showMarkerInfo(hit.object.userData.markerId as string);
+        if (!placingMode.value && sc) {
+          const markerId = findClickedMarkerId(sc);
+          if (markerId) {
+            showMarkerInfo(markerId);
+            forceViewerRender();
             return;
           }
         }
@@ -595,44 +733,34 @@
           }
         }
 
-        viewer.renderer.render(viewer.scene, viewer.camera);
-        viewer.getCssRender()?.render(viewer.scene, viewer.camera);
+        forceViewerRender();
       });
 
       const container = document.getElementById(containerId);
       if (container) {
+        let lastMoveEvent: MouseEvent | null = null;
         container.addEventListener('mousemove', (e: MouseEvent) => {
           if (!viewer) return;
-          const sc = scaledScreenCoord(e, container);
-          if (floatingDot && placingMode.value) {
-            floatingDot.style.left = sc.x + '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;
-          }
+          lastMoveEvent = e;
+          if (mouseMoveRaf) return;
+          mouseMoveRaf = requestAnimationFrame(() => {
+            mouseMoveRaf = 0;
+            if (!lastMoveEvent || !viewer) return;
+            const sc = updateCurrentCoord(lastMoveEvent, container);
+            if (sc && floatingDot && placingMode.value) {
+              floatingDot.style.left = sc.x + 'px';
+              floatingDot.style.top = sc.y + 'px';
+            }
+          });
         });
       }
 
-      // 滚轮缩放 / 平移时,信息悬浮框跟随布点
-      viewer.addEventListener(ViewerEvent.CameraChange, () => {
-        updateInfoPanelPosition();
-      });
-      viewer.addEventListener(ViewerEvent.AfterRender, () => {
-        if (infoPanel && activeInfoMarkerId) updateInfoPanelPosition();
-      });
+      scheduleRestoreMarkers();
     } catch (err) {
       console.error('[DWG] 初始化失败:', err);
     }
   });
 
-  /**
-   * 监听设备列表变化
-   * 当外部传入的设备数据更新时,同步布点状态到列表(保证按钮状态正确)
-   */
   watch(
     deviceList,
     () => {
@@ -642,31 +770,25 @@
     { deep: true }
   );
 
-  /** 键盘事件处理:Esc键退出布点模式 */
-  const onKeydown = (e: KeyboardEvent) => {
-    if (e.key === 'Escape' && placingMode.value) exitPlacingMode();
-  };
-  onMounted(() => window.addEventListener('keydown', onKeydown));
-
-  /**
-   * 组件卸载时清理资源
-   * 移除事件监听、销毁Viewer实例、释放所有THREE.js资源
-   */
   onUnmounted(() => {
     window.removeEventListener('keydown', onKeydown);
+    if (mouseMoveRaf) cancelAnimationFrame(mouseMoveRaf);
+    unbindInfoPanelFollow();
     hideInfoPanel();
     floatingDot?.remove();
     floatingDot = null;
     if (viewer) {
       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();
       });
       markersMap.clear();
+      sharedCircleGeom?.dispose();
+      sharedCircleGeom = null;
+      markerIconTexture?.dispose();
+      markerIconTexture = null;
       viewer.destroy?.();
       viewer = null;
     }
@@ -674,7 +796,6 @@
 </script>
 
 <style lang="less" scoped>
-  /* 最外层容器:占满父级、隐藏溢出 */
   .content {
     width: 100%;
     height: 100%;
@@ -682,20 +803,13 @@
     overflow: hidden;
   }
 
-  /* 视图区域:canvas 的父容器 */
-  .viewer-area {
-    width: 100%;
-    height: 100%;
-    position: relative;
-  }
-  /* 图纸渲染 canvas 容器 */
+  .viewer-area,
   .viewer-canvas {
     width: 100%;
     height: 100%;
     position: relative;
   }
 
-  /* 底部坐标栏:显示当前鼠标在世界坐标中的位置 */
   .coord-bar {
     position: absolute;
     bottom: 8px;
@@ -708,17 +822,16 @@
     font-size: 13px;
     font-family: Consolas, Monaco, monospace;
     z-index: 200;
-    pointer-events: none; /* 不阻挡鼠标事件,让事件穿透到 canvas */
+    pointer-events: none;
     white-space: nowrap;
     > span {
       margin: 0 4px;
     }
     .placing-hint {
-      color: #faad14; /* 放置模式下的提示文字颜色 */
+      color: #faad14;
     }
   }
 
-  /* 右侧设备列表面板(毛玻璃效果) */
   .device-panel {
     position: absolute;
     top: 8px;
@@ -732,11 +845,9 @@
     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;
@@ -755,7 +866,6 @@
         color: rgba(255, 255, 255, 0.5);
       }
     }
-    /* 面板内容区:设备表格 */
     .panel-body {
       padding: 6px;
       background: transparent;

+ 3 - 2
src/views/vent/monitorManager/hsqHome/components/DeviceCenter.vue

@@ -193,7 +193,8 @@ onUnmounted(() => {
 }
 
 .long-list1 {
-  --image-box-bg: url('@/assets/images/home-container/configurable/hsq/2-11.png');
+  --image-box-bg: url('@/assets/images/home-container/configurable/hsq/2-5.png');
+  // --image-box-bg: url('@/assets/images/home-container/configurable/hsq/2-11.png');
   --image-box-bg1: url('@/assets/images/home-container/configurable/hsq/2-13.png');
   --image-box-bg2: url('@/assets/images/home-container/configurable/hsq/2-14.png');
   --image-box-bg3: url('@/assets/images/home-container/configurable/hsq/2-15.png');
@@ -218,7 +219,7 @@ onUnmounted(() => {
   .title-l {
     width: 177px;
     height: 100%;
-    color: #fc002a;
+  color: #01fefc;
     font-weight: bolder;
     padding-left: 16px;
     background: var(--image-box-bg) no-repeat;

+ 2 - 1
src/views/vent/monitorManager/hsqHome/components/LinkRuleTable.vue

@@ -141,7 +141,8 @@ const filterDeviceColumns = computed(() => {
 });
 
 const realTableData = computed(() => {
-  return props.config.type === 'D' ? alarmList.value : tableList.value;
+  // return props.config.type === 'D' ? alarmList.value : tableList.value;
+   return props.config.type === 'D' ? '' : tableList.value;
 });
 const props = defineProps<{
   config: {

+ 3 - 7
src/views/vent/monitorManager/hsqHome/components/QuickAction.vue

@@ -50,10 +50,10 @@ let props = defineProps({
   --image-card-bg2: url('@/assets/images/home-container/configurable/hsq/1-15.png');
   display: flex;
   flex-direction: column;
-  justify-content: space-between;
+  justify-content: center;
   width: 100%;
   height: 100%;
-  padding: 15px 15px 0px 15px;
+  padding: 5px 15px 0px 15px;
   box-sizing: border-box;
 
   .basic-box {
@@ -63,11 +63,7 @@ let props = defineProps({
   }
 
   .basic-boxT {
-    height: calc(100% - 50px);
-  }
-
-  .basic-boxB {
-    height: 50px;
+   margin-bottom: 5px;
   }
 
   .nav-box {

+ 24 - 18
src/views/vent/monitorManager/hsqHome/components/WarnCenTop.vue

@@ -28,32 +28,28 @@
             <div class="warn-item">
               <div class="item-label">报警级别 : </div>
               <div class="item-val"
-                :style="{ color: warnData.alarmLevel == 101 ? '#00d8ff' : warnData.alarmLevel == 102 ? '#fcfc22' : warnData.alarmLevel == 103 ? '#ff7010' : warnData.alarmLevel == 104 ? '#df4e43' : warnData.alarmLevel == 201 ? '#ff0000' : '' }">
-                {{ warnData.alarmLevel == 101 ? '低风险' : warnData.alarmLevel ==
+                :style="{ color: warnDatas.alarmLevel == 101 ? '#00d8ff' : warnDatas.alarmLevel == 102 ? '#fcfc22' : warnDatas.alarmLevel == 103 ? '#ff7010' : warnDatas.alarmLevel == 104 ? '#df4e43' : warnDatas.alarmLevel == 201 ? '#ff0000' : '' }">
+                {{ warnDatas.alarmLevel == 101 ? '低风险' : warnDatas.alarmLevel ==
                   102 ? '一般风险' :
-                  warnData.alarmLevel == 103 ? '较大风险' : warnData.alarmLevel == 104 ? '重大风险' : warnData.alarmLevel == 201 ?
+                  warnDatas.alarmLevel == 103 ? '较大风险' : warnDatas.alarmLevel == 104 ? '重大风险' : warnDatas.alarmLevel == 201 ?
                     '报警' :
-                '-' }}</div>
+                    '-' }}</div>
             </div>
             <div class="warn-item">
               <div class="item-label">报警时间 : </div>
-              <div class="item-val"> {{ warnData.starttime || '-' }}</div>
+              <div class="item-val"> {{ warnDatas.starttime || '-' }}</div>
             </div>
             <div class="warn-item">
               <div class="item-label">报警位置 : </div>
-              <div class="item-val"> {{ warnData.alarmName || '-' }}</div>
+              <div class="item-val"> {{ warnDatas.alarmName || '-' }}</div>
             </div>
             <div class="warn-item">
-              <div class="item-label">监测设备 : </div>
-              <div class="item-val"> {{ warnData.device || '-' }}</div>
+              <div class="item-label">预警设备 : </div>
+              <div class="item-val"> {{ warnDatas.deviceName || '-' }}</div>
             </div>
             <div class="warn-item">
-              <div class="item-label ">最高温度 : </div>
-              <div class="item-val text-val"> {{ warnData.maxtemp || '-' }}</div>
-            </div>
-            <div class="warn-item">
-              <div class="item-label">平均速度 : </div>
-              <div class="item-val text-val1"> {{ warnData.speed || '-' }}</div>
+              <div class="item-label ">预警温度 : </div>
+              <div class="item-val text-val"> {{ `${warnDatas.temp}℃` || '-' }}</div>
             </div>
           </div>
         </basicBorder>
@@ -89,7 +85,7 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref } from 'vue'
+import { reactive, ref, watch } from 'vue'
 import basicBorder from './basicBorder.vue'
 import { getAssetURL } from '/@/utils/ui';
 
@@ -101,6 +97,7 @@ let props = defineProps({
 })
 
 let deviceModel = ref('-')
+let warnDatas = ref({}) as any
 let picList = ref([
   { src: getAssetURL('home-container/configurable/hsq/4-1.png'), label: '1.报警触发' },
   { src: getAssetURL('home-container/configurable/hsq/6-1.png'), label: '2.系统确认' },
@@ -109,6 +106,15 @@ let picList = ref([
   { src: getAssetURL('home-container/configurable/hsq/4-5.png'), label: '5.确认解除' },
 ])
 
+watch(() => props.warnData, (newVal) => {
+  let data = JSON.parse(JSON.stringify(newVal.autoContent))
+  let obj = {
+    deviceName: data.substring(data.indexOf('【') + 1, data.indexOf('】')),
+    temp: data.substring(data.lastIndexOf('【') + 1, data.lastIndexOf('】')),
+  }
+  warnDatas.value = Object.assign({}, newVal, obj)
+})
+
 </script>
 
 <style lang="less" scoped>
@@ -204,7 +210,7 @@ let picList = ref([
   .warn-item {
     display: flex;
     align-items: center;
-    height: 23px;
+    height: 28px;
     background: linear-gradient(to right, #134c77, transparent);
     margin-bottom: 8px;
     padding: 0px 15px;
@@ -217,11 +223,11 @@ let picList = ref([
 
   .item-label {
     margin-right: 5px;
-    font-size: 13px;
+    // font-size: 13px;
   }
 
   .item-val {
-    font-size: 13px;
+    // font-size: 13px;
   }
 
   .text-val {

+ 3 - 3
src/views/vent/monitorManager/hsqHome/fireMonitor.vue

@@ -97,7 +97,7 @@ let envData = reactive({})
 
 /** 快捷操作面板数据 */
 let quickData = reactive({
-  time: '128天06:32:18',
+  time: '8天6小时32分18秒',
   sjsx: '实时',
   wlyc: '12s',
 })
@@ -127,7 +127,7 @@ function getMonitor(flag?: boolean) {
  * 调用 getManagesysDeviceNum 接口,根据当前系统ID获取设备数量统计信息
  */
 async function getManagesysDeviceNumTj() {
-  const res = await getManagesysDeviceNum({ sysId: systemId.value });
+  const res = await getManagesysDeviceNum({ sysId: systemId.value,deviceKinds:"duaSpeCamera" });
   galleryData.value = res || {};
 }
 /**
@@ -158,7 +158,7 @@ async function getDeviceMonitor() {
     return {
       label: el.datalist[0].typeName,
       value: Object.keys(el.datalist[0].readData).filter(v => v.endsWith('val') || v.endsWith('ture'))[0],
-      unit: ''
+      unit: el.datalist[0].typeName=='温度传感器' ? '℃' : el.datalist[0].typeName=='氧气传感器' ? '%' : el.datalist[0].typeName=='CO传感器' ? 'ppm' : el.datalist[0].typeName=='瓦斯传感器' ? '%' : el.datalist[0].typeName=='烟雾传感器' ? '' : ''
     }
   })
   envData = Object.assign({}, ...res.msgTxt.filter(v => v.type.indexOf('modelsensor') != -1).map(el => el.datalist[0].readData))

+ 7 - 0
src/views/vent/monitorManager/hsqHome/hsqHome.data.ts

@@ -139,6 +139,13 @@ export let titleOption = [
   { label: '创建时间', value: 'createTime' },
   { label: '日志类型', value: 'logType_dictText' },
 ]
+export let titleLookOption = [
+  { label: '操作人ID', value: 'userid' },
+  { label: '操作人', value: 'username' },
+  { label: '日志内容', value: 'logContent' },
+  { label: 'IP', value: 'ip' },
+  { label: '创建时间', value: 'createTime' },
+]
 
 //今日登录统计
 export let dayLoginConfig: any = {

+ 2 - 2
src/views/vent/monitorManager/hsqHome/systemManger.vue

@@ -17,7 +17,7 @@
         </SystemLeftBottom>
       </div>
       <div class="system-box-b">
-          <SystemLeftBottom title="浏览日志" :titleOption="titleOption" :data="lookLogList" >
+          <SystemLeftBottom title="浏览日志" :titleOption="titleLookOption" :data="lookLogList" >
         </SystemLeftBottom>
       </div>
       <!-- <div class="system-box-b">
@@ -36,7 +36,7 @@ import SystemLeftTop from './components/SystemLeftTop.vue';
 import SystemRightTop from './components/SystemRightTop.vue';
 import SystemLeftBottom from './components/SystemLeftBottom.vue';
 import { userList, LongList, queryRoleAndUserNum } from './hsqHome.api.js'
-import { optionUser, titleOption } from './hsqHome.data.js'
+import { optionUser, titleOption, titleLookOption } from './hsqHome.data.js'
 
 /** 用户列表数据,供左上角用户表格展示 */
 let listData = ref([])