4 Commits 6892ce8981 ... 57b8ff7fde

Autor SHA1 Mensagem Data
  画安 57b8ff7fde 子区蒙层中心显示子区名称,水印风格自适应区域大小 1 semana atrás
  画安 a376ced424 统一地图标记与 API 的路口状态数据源,离线标记显示提示弹窗 1 semana atrás
  画安 d8e78fd03e 地图图标跟随缩放动态调整大小 1 semana atrás
  画安 0b5c876aa4 调整地图标记点大小和文字可读性 1 semana atrás

+ 62 - 33
src/components/TongzhouTrafficMap.vue

@@ -43,6 +43,7 @@
 
 <script>
 import AMapLoader from '@amap/amap-jsapi-loader';
+import { getIntersectionCategory } from '@/mock/api';
 
 export default {
   name: "TrafficMap",
@@ -84,7 +85,7 @@ export default {
       ],
       intersectionData: [],
       statusIntersections: {},
-      currentZoomSize: 14, // 保存当前的动态尺寸
+      currentZoomSize: 20, // 保存当前的动态尺寸(与 zoom=15 基准一致)
       markerById: {}, // ID → marker 索引,加速 focusById 查找
       subAreaOverlays: [], // 子区蒙层覆盖物
     };
@@ -187,12 +188,12 @@ export default {
     // 自动计算并下发给所有 CSS 的变量字典
     mapCssVars() {
       const size = this.currentZoomSize;
-      const specialSize = Math.max(16, size * 1.5);
+      const specialSize = Math.max(12, size * 1.2);
       return {
         '--dot-size': `${size}px`,
-        '--dot-padding': size >= 12 ? '2px' : '0px',
-        '--text-display': size >= 12 ? 'flex' : 'none',
-        '--text-size': `${Math.max(10, size - 2)}px`,
+        '--dot-padding': size >= 14 ? '4px' : '0px',
+        '--text-display': size >= 14 ? 'flex' : 'none',
+        '--text-size': `${Math.max(10, size - 4)}px`,
         '--special-size': `${specialSize}px`
       };
     }
@@ -225,26 +226,19 @@ export default {
      * 将真实路口数据按状态类型分类
      */
     classifyIntersectionsByStatus() {
-      const remainingData = this.intersectionData;
-      const maxAbnormalCount = 4;
-      const abnormalTotal = maxAbnormalCount * 3;
-      const normalData = remainingData.slice(0, remainingData.length - abnormalTotal);
-      const abnormalData = remainingData.slice(remainingData.length - abnormalTotal);
-      const normalChunk = Math.ceil(normalData.length / 6);
-
-      this.statusIntersections = {
-        "中心计划": normalData.slice(0, normalChunk),
-        "干线协调": [],
-        "勤务路线": [],
-        "定周期控制": normalData.slice(normalChunk, normalChunk * 2),
-        "感应控制": normalData.slice(normalChunk * 2, normalChunk * 3),
-        "自适应控制": normalData.slice(normalChunk * 3, normalChunk * 4),
-        "手动控制": normalData.slice(normalChunk * 4, normalChunk * 5),
-        "特殊控制": normalData.slice(normalChunk * 5),
-        "离线": abnormalData.slice(0, maxAbnormalCount),
-        "降级": abnormalData.slice(maxAbnormalCount, maxAbnormalCount * 2),
-        "故障": abnormalData.slice(maxAbnormalCount * 2, maxAbnormalCount * 3)
+      const categories = {
+        "中心计划": [], "干线协调": [], "勤务路线": [],
+        "定周期控制": [], "感应控制": [], "自适应控制": [],
+        "手动控制": [], "特殊控制": [],
+        "离线": [], "降级": [], "故障": []
       };
+      for (const item of this.intersectionData) {
+        const category = getIntersectionCategory(item["路口编号"]);
+        if (categories[category]) {
+          categories[category].push(item);
+        }
+      }
+      this.statusIntersections = categories;
     },
 
     /**
@@ -314,6 +308,7 @@ export default {
 
         this.map.on('complete', () => {
           if (!this.isComponentDestroyed) {
+            this.currentZoomSize = this.getDotSizeByZoom();
             this.drawStaticRoutes();
           }
         });
@@ -618,9 +613,9 @@ export default {
             overlays.push(new this.AMap.Marker({
               position: splitPoint,
               zIndex: 150,
-              offset: new this.AMap.Pixel(-12, -12),
+              anchor: 'center',
               bubble: true,
-              content: `<div class="duty-progress-node" style="width:24px;height:24px;border-radius:50%;background:#BC301D;border:3px solid #fff;box-shadow:0 0 0 3px rgba(188,48,29,0.4);display:flex;justify-content:center;align-items:center;cursor:default;"><div style="width:8px;height:8px;border-radius:50%;background:#fff;"></div></div>`
+              content: `<div class="duty-progress-node" style="width:var(--special-size);height:var(--special-size);border-radius:50%;background:#BC301D;border:3px solid #fff;box-shadow:0 0 0 3px rgba(188,48,29,0.4);display:flex;justify-content:center;align-items:center;cursor:default;"><div style="width:30%;height:30%;border-radius:50%;background:#fff;"></div></div>`
             }));
           }
 
@@ -939,9 +934,9 @@ export default {
     getDotSizeByZoom() {
       if (!this.map) return 14;
       const zoom = this.map.getZoom();
-      // 基准:zoom=15时是14px。每缩小一级减小3px。
-      const size = 14 + (zoom - 15) * 3;
-      return Math.min(Math.max(6, size), 28); // 最小值设为 6px
+      // 基准:zoom=15时是20px。每缩小一级减小3px。
+      const size = 20 + (zoom - 15) * 3;
+      return Math.min(Math.max(6, size), 32); // 最小值设为 6px
     },
 
     /**
@@ -979,8 +974,8 @@ export default {
           `;
         } else if (isPassed) {
           markerContent = `
-            <div class="pure-light-node" style="width: 14px; height: 14px; background: #5A5A5A; border: 1.5px solid rgba(255,255,255,0.25); box-sizing: content-box; display: flex; justify-content: center; align-items: center; color: #888; border-radius: 50%; cursor: pointer; padding: 2px; opacity: 0.55;">
-              <span style="transform: scale(0.8); font-weight: bold; font-size: 12px;">勤</span>
+            <div class="pure-light-node" style="width: var(--dot-size); height: var(--dot-size); background: #5A5A5A; border: 1.5px solid rgba(255,255,255,0.25); box-sizing: content-box; display: flex; justify-content: center; align-items: center; color: #888; border-radius: 50%; cursor: pointer; padding: var(--dot-padding); opacity: 0.55;">
+              <span style="display: var(--text-display); font-weight: bold; font-size: var(--text-size);">勤</span>
             </div>
           `;
         } else if (isAbnormal) {
@@ -993,7 +988,7 @@ export default {
         } else {
           markerContent = `
             <div class="pure-light-node ${isRoute ? 'route-node' : ''}" style="width: var(--dot-size); height: var(--dot-size); background: ${config.color || '#999'}; box-shadow: ${isRoute ? 'none' : `0 0 8px ${config.color}`}; border: ${isRoute ? 'none' : '1.5px solid rgba(255,255,255,0.7)'}; box-sizing: border-box; display: flex; justify-content: center; align-items: center; color: #fff; border-radius: 50%; cursor: pointer; padding: var(--dot-padding);">
-              <span style="display: var(--text-display); transform: scale(0.8); font-weight: bold; font-size: var(--text-size);">${displayText}</span>
+              <span style="display: var(--text-display); font-weight: bold; font-size: var(--text-size);">${displayText}</span>
             </div>
           `;
         }
@@ -1365,7 +1360,7 @@ export default {
      * 对外暴露:为指定子区路口列表绘制凸包多边形蒙层,点击不同子区时自动替换上一个
      * @param {Array<{lng: number, lat: number}>} leaves - 子区内所有叶子路口节点
      */
-    drawSubAreaCircle(leaves) {
+    drawSubAreaCircle(leaves, label) {
       if (!this.isMapReady() || !leaves || leaves.length === 0) return;
 
       this.clearSubAreaOverlays();
@@ -1399,6 +1394,40 @@ export default {
 
       this.subAreaOverlays.push(polygon);
       this.map.add(polygon);
+
+      // 在凸包重心位置显示子区名称(水印风格,字号随区域大小缩放)
+      if (label) {
+        // 用地图坐标转像素,精确计算区域在屏幕上的实际大小
+        const lngs = paddedHull.map(p => p[0]);
+        const lats = paddedHull.map(p => p[1]);
+        const topLeft = this.map.lngLatToContainer([Math.min(...lngs), Math.max(...lats)]);
+        const bottomRight = this.map.lngLatToContainer([Math.max(...lngs), Math.min(...lats)]);
+        const pixelW = Math.abs(bottomRight.x - topLeft.x);
+        const pixelH = Math.abs(bottomRight.y - topLeft.y);
+        // 字号以横向宽度为基准,按文字长度均分,确保不超出区域
+        const charCount = label.length || 1;
+        const fontSize = Math.min(Math.max(12, Math.round(pixelW / (charCount + 1))), 48);
+
+        const text = new this.AMap.Text({
+          text: label,
+          position: [cx, cy],
+          anchor: 'center',
+          zIndex: 11,
+          style: {
+            'background': 'transparent',
+            'border': 'none',
+            'color': 'rgba(255, 255, 255, 0.25)',
+            'font-size': `${fontSize}px`,
+            'font-weight': 'bold',
+            'padding': '0',
+            'letter-spacing': '2px',
+            'white-space': 'nowrap',
+            'pointer-events': 'none',
+          },
+        });
+        this.subAreaOverlays.push(text);
+        this.map.add(text);
+      }
     },
 
     /**

+ 38 - 0
src/components/ui/OfflineTip.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="offline-tip">
+    <span class="offline-dot" :style="{ background: dotColor }"></span>
+    <span class="offline-text">设备{{ status }}</span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'OfflineTip',
+  props: {
+    status: { type: String, default: '离线' },
+    road: { type: String, default: '' },
+  },
+  computed: {
+    dotColor() {
+      const map = { '离线': '#7A7A7A', '降级': '#D9C13B', '故障': '#FF3938' };
+      return map[this.status] || '#7A7A7A';
+    }
+  }
+};
+</script>
+
+<style scoped>
+.offline-tip {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #e0e8f0;
+  font-size: 14px;
+}
+.offline-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+</style>

+ 3 - 1
src/layouts/DashboardLayout.vue

@@ -107,6 +107,7 @@ import SpecialTaskMonitorPanel from '@/components/ui/SpecialTaskMonitorPanel.vue
 import ChangePassword from '@/components/ui/ChangePassword.vue';
 import CrossingMultiView from '@/components/ui/CrossingMultiView.vue';
 import CrossingDetailHeader from '@/components/ui/CrossingDetailHeader.vue';
+import OfflineTip from '@/components/ui/OfflineTip.vue';
 
 export default {
     name: 'DashboardLayout',
@@ -134,7 +135,8 @@ export default {
         SpecialTaskMonitorPanel,
         ChangePassword,
         CrossingMultiView,
-        CrossingDetailHeader
+        CrossingDetailHeader,
+        OfflineTip
     },
     provide() {
         return {

+ 15 - 0
src/mock/api.js

@@ -44,6 +44,21 @@ function _getDeviceStatus(id) {
   return statusList[seed % statusList.length]
 }
 
+/**
+ * 根据路口ID生成稳定的地图分类(在线→控制方式,离线→异常类型)
+ * 地图标记和 API 共用,确保状态一致
+ */
+export function getIntersectionCategory(id) {
+  const status = _getDeviceStatus(id)
+  const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
+  if (status === '离线') {
+    const abnormalTypes = ['离线', '降级', '故障']
+    return abnormalTypes[seed % abnormalTypes.length]
+  }
+  const normalTypes = ['中心计划', '定周期控制', '感应控制', '自适应控制', '手动控制', '特殊控制']
+  return normalTypes[seed % normalTypes.length]
+}
+
 /** 基于当前秒数产生稳定随机(同一秒内多次调用返回相同值) */
 function seededRand(seed) {
   const x = Math.sin(seed) * 10000

+ 35 - 8
src/views/SpecialSituationMonitoring.vue

@@ -170,10 +170,29 @@ export default {
             let nodeData = {
                 id: mapData.id || (mapData.position[0] + mapData.position[1]),
                 label: mapData.road,
-                // 反算为设计稿坐标(SmartDialog 内部会再乘 scale)
                 pixelX: pixel ? Math.round(pixel.x / scale) : 950,
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
+            // 离线/降级/故障:显示小弹窗提示状态
+            const abnormalNames = ['离线', '降级', '故障'];
+            if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
+                const statusText = mapData.statusLabel || mapData.name;
+                this.$refs.layout.openDialog({
+                    id: 'offline_tip_' + nodeData.id,
+                    title: nodeData.label,
+                    component: 'OfflineTip',
+                    width: 260,
+                    height: 100,
+                    center: false,
+                    showClose: false,
+                    noPadding: false,
+                    draggable: false,
+                    resizable: false,
+                    position: { x: (nodeData.pixelX || 950) + 10, y: nodeData.pixelY || 430 },
+                    data: { status: statusText, road: nodeData.label },
+                });
+                return;
+            }
             console.log(nodeData);
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showOverviewDalogs(nodeData);
@@ -184,20 +203,28 @@ export default {
         // 处理地图鼠标滑出事件
         handleMapCrossingMouseout(mapData) {
             console.log('父组件接收到了地图路口鼠标滑出事件:', mapData);
-            if (this.activeLeftTab === 'overview' && mapData) { // 总览
-                const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            if (!mapData) return;
+            const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            this.$refs.layout.handleDialogClose('offline_tip_' + id);
+            if (this.activeLeftTab === 'overview') { // 总览
                 this.$refs.layout.handleDialogClose('crossing3_' + id);
-            } else if (this.activeLeftTab === 'crossing' && mapData) { // 路口
-                
-                const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            } else if (this.activeLeftTab === 'crossing') { // 路口
                 this.$refs.layout.handleDialogClose('crossing3_' + id);
             }
         },
         // 处理地图点击事件
         handleMapCrossingClick(mapData, lnglat, pixel) {
             console.log('父组件接收到了地图路口点击事件:', mapData);
-            console.log('父组件接收到了地图路口点击事件:', lnglat);
-            console.log('父组件接收到了地图路口点击事件:', pixel);
+            // 离线/降级/故障状态不弹详情
+            const abnormalNames = ['离线', '降级', '故障'];
+            if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
+                this.$msg({
+                    title: '提示',
+                    message: `路口「${mapData.road || mapData.id}」设备${mapData.statusLabel || mapData.name},无法查看详情`,
+                    duration: 3000,
+                });
+                return;
+            }
             // 组装模拟数据
             const scale = window.innerWidth / 1920;
             let nodeData = {

+ 37 - 9
src/views/StatusMonitoring.vue

@@ -209,10 +209,29 @@ export default {
             let nodeData = {
                 id: mapData.id || (mapData.position[0] + mapData.position[1]),
                 label: mapData.road,
-                // 反算为设计稿坐标(SmartDialog 内部会再乘 scale)
                 pixelX: pixel ? Math.round(pixel.x / scale) : 950,
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
+            // 离线/降级/故障:显示小弹窗提示状态
+            const abnormalNames = ['离线', '降级', '故障'];
+            if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
+                const statusText = mapData.statusLabel || mapData.name;
+                this.$refs.layout.openDialog({
+                    id: 'offline_tip_' + nodeData.id,
+                    title: nodeData.label,
+                    component: 'OfflineTip',
+                    width: 260,
+                    height: 100,
+                    center: false,
+                    showClose: false,
+                    noPadding: false,
+                    draggable: false,
+                    resizable: false,
+                    position: { x: (nodeData.pixelX || 950) + 10, y: nodeData.pixelY || 430 },
+                    data: { status: statusText, road: nodeData.label },
+                });
+                return;
+            }
             console.log(nodeData);
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showOverviewDalogs(nodeData);
@@ -223,20 +242,29 @@ export default {
         // 处理地图鼠标滑出事件
         handleMapCrossingMouseout(mapData) {
             console.log('父组件接收到了地图路口鼠标滑出事件:', mapData);
-            if (this.activeLeftTab === 'overview' && mapData) { // 总览
-                const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            if (!mapData) return;
+            const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            // 关闭离线提示小弹窗
+            this.$refs.layout.handleDialogClose('offline_tip_' + id);
+            if (this.activeLeftTab === 'overview') { // 总览
                 this.$refs.layout.handleDialogClose('crossing3_' + id);
-            } else if (this.activeLeftTab === 'crossing' && mapData) { // 路口
-
-                const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            } else if (this.activeLeftTab === 'crossing') { // 路口
                 this.$refs.layout.handleDialogClose('crossing3_' + id);
             }
         },
         // 处理地图点击事件
         handleMapCrossingClick(mapData, lnglat, pixel) {
             console.log('父组件接收到了地图路口点击事件:', mapData);
-            console.log('父组件接收到了地图路口点击事件:', lnglat);
-            console.log('父组件接收到了地图路口点击事件:', pixel);
+            // 离线/降级/故障状态不弹详情
+            const abnormalNames = ['离线', '降级', '故障'];
+            if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
+                this.$msg({
+                    title: '提示',
+                    message: `路口「${mapData.road || mapData.id}」设备${mapData.statusLabel || mapData.name},无法查看详情`,
+                    duration: 3000,
+                });
+                return;
+            }
             // 组装模拟数据
             const scale = window.innerWidth / 1920;
             let nodeData = {
@@ -328,7 +356,7 @@ export default {
             const map = this.$refs.trafficMapRef.map;
             if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
             // 绘制该子区的圆形蒙层(自动替换上一个)
-            this.$refs.trafficMapRef.drawSubAreaCircle(leaves);
+            this.$refs.trafficMapRef.drawSubAreaCircle(leaves, nodeData.label);
         },
         // 处理菜单点击
         handleMenuClick(nodeData) {

+ 35 - 8
src/views/TrunkCoordination.vue

@@ -174,10 +174,29 @@ export default {
             let nodeData = {
                 id: mapData.id || (mapData.position[0] + mapData.position[1]),
                 label: mapData.road,
-                // 反算为设计稿坐标(SmartDialog 内部会再乘 scale)
                 pixelX: pixel ? Math.round(pixel.x / scale) : 950,
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
+            // 离线/降级/故障:显示小弹窗提示状态
+            const abnormalNames = ['离线', '降级', '故障'];
+            if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
+                const statusText = mapData.statusLabel || mapData.name;
+                this.$refs.layout.openDialog({
+                    id: 'offline_tip_' + nodeData.id,
+                    title: nodeData.label,
+                    component: 'OfflineTip',
+                    width: 260,
+                    height: 100,
+                    center: false,
+                    showClose: false,
+                    noPadding: false,
+                    draggable: false,
+                    resizable: false,
+                    position: { x: (nodeData.pixelX || 950) + 10, y: nodeData.pixelY || 430 },
+                    data: { status: statusText, road: nodeData.label },
+                });
+                return;
+            }
             console.log(nodeData);
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showOverviewDalogs(nodeData);
@@ -188,20 +207,28 @@ export default {
         // 处理地图鼠标滑出事件
         handleMapCrossingMouseout(mapData) {
             console.log('父组件接收到了地图路口鼠标滑出事件:', mapData);
-            if (this.activeLeftTab === 'overview' && mapData) { // 总览
-                const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            if (!mapData) return;
+            const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            this.$refs.layout.handleDialogClose('offline_tip_' + id);
+            if (this.activeLeftTab === 'overview') { // 总览
                 this.$refs.layout.handleDialogClose('crossing3_' + id);
-            } else if (this.activeLeftTab === 'crossing' && mapData) { // 路口
-                
-                const id = mapData.id || (mapData.position[0] + mapData.position[1]);
+            } else if (this.activeLeftTab === 'crossing') { // 路口
                 this.$refs.layout.handleDialogClose('crossing3_' + id);
             }
         },
         // 处理地图点击事件
         handleMapCrossingClick(mapData, lnglat, pixel) {
             console.log('父组件接收到了地图路口点击事件:', mapData);
-            console.log('父组件接收到了地图路口点击事件:', lnglat);
-            console.log('父组件接收到了地图路口点击事件:', pixel);
+            // 离线/降级/故障状态不弹详情
+            const abnormalNames = ['离线', '降级', '故障'];
+            if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
+                this.$msg({
+                    title: '提示',
+                    message: `路口「${mapData.road || mapData.id}」设备${mapData.statusLabel || mapData.name},无法查看详情`,
+                    duration: 3000,
+                });
+                return;
+            }
             // 组装模拟数据
             const scale = window.innerWidth / 1920;
             let nodeData = {