2 Revize c1a746d309 ... 51965cbf74

Autor SHA1 Zpráva Datum
  画安 51965cbf74 将子区蒙层从 AMap.Circle 改为凸包多边形,更贴合实际路口分布 před 2 týdny
  画安 31cd2ad885 状态监控总览:点击子区时在地图上绘制圆形蒙层 před 2 týdny

+ 3 - 0
src/api/index.js

@@ -109,3 +109,6 @@ export const apiGetCrossingTopCharts = () =>
 
 
 export const apiGetOverviewTopCharts = () =>
 export const apiGetOverviewTopCharts = () =>
   http.get('/overview/top-charts')
   http.get('/overview/top-charts')
+
+export const apiGetMapLegendConfig = () =>
+  http.get('/map/legend-config')

+ 93 - 3
src/components/TongzhouTrafficMap.vue

@@ -86,6 +86,7 @@ export default {
       statusIntersections: {},
       statusIntersections: {},
       currentZoomSize: 14, // 保存当前的动态尺寸
       currentZoomSize: 14, // 保存当前的动态尺寸
       markerById: {}, // ID → marker 索引,加速 focusById 查找
       markerById: {}, // ID → marker 索引,加速 focusById 查找
+      subAreaOverlays: [], // 子区蒙层覆盖物
     };
     };
   },
   },
   mounted() {
   mounted() {
@@ -141,7 +142,10 @@ export default {
       this.routeGroups = {};
       this.routeGroups = {};
     }
     }
 
 
-    // 4. 销毁地图实例并清空引用
+    // 4. 清理子区蒙层
+    this.clearSubAreaOverlays();
+
+    // 6. 销毁地图实例并清空引用
     if (this.map) {
     if (this.map) {
       try {
       try {
         this.map.destroy();
         this.map.destroy();
@@ -151,7 +155,7 @@ export default {
       this.map = null;
       this.map = null;
     }
     }
 
 
-    // 5. 清理其他引用
+    // 7. 清理其他引用
     this.AMap = null;
     this.AMap = null;
     this.driving = null;
     this.driving = null;
     if (this.infoCloseTimer) {
     if (this.infoCloseTimer) {
@@ -1355,7 +1359,93 @@ export default {
     lngLatToPixel(lng, lat) {
     lngLatToPixel(lng, lat) {
         if (!this.map) return null;
         if (!this.map) return null;
         return this.map.lngLatToContainer([lng, lat]);
         return this.map.lngLatToContainer([lng, lat]);
-    }
+    },
+
+    /**
+     * 对外暴露:为指定子区路口列表绘制凸包多边形蒙层,点击不同子区时自动替换上一个
+     * @param {Array<{lng: number, lat: number}>} leaves - 子区内所有叶子路口节点
+     */
+    drawSubAreaCircle(leaves) {
+      if (!this.isMapReady() || !leaves || leaves.length === 0) return;
+
+      this.clearSubAreaOverlays();
+
+      const pts = leaves.map(n => [Number(n.lng), Number(n.lat)]);
+      if (pts.length < 3) return;
+
+      const hull = this._convexHull(pts);
+      if (hull.length < 3) return;
+
+      // 以凸包重心为基准,向外扩展 10% 留出视觉间距
+      const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;
+      const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;
+      const paddedHull = hull.map(p => [
+        cx + (p[0] - cx) * 1.1,
+        cy + (p[1] - cy) * 1.1,
+      ]);
+
+      const polygon = new this.AMap.Polygon({
+        path: paddedHull,
+        fillColor: '#4A9EFF',
+        fillOpacity: 0.15,
+        strokeColor: '#4A9EFF',
+        strokeOpacity: 0.6,
+        strokeWeight: 1,
+        strokeStyle: 'dashed',
+        strokeDasharray: [4, 4],
+        zIndex: 10,
+        bubble: true,
+      });
+
+      this.subAreaOverlays.push(polygon);
+      this.map.add(polygon);
+    },
+
+    /**
+     * Graham Scan 凸包算法
+     * @param {Array<[number, number]>} points - 经纬度点数组
+     * @returns {Array<[number, number]>} 凸包顶点(逆时针)
+     */
+    _convexHull(points) {
+      if (points.length < 3) return [...points];
+
+      const p0 = points.reduce((best, p) =>
+        p[1] < best[1] || (p[1] === best[1] && p[0] < best[0]) ? p : best
+      );
+
+      const cross = (o, a, b) =>
+        (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
+
+      const dist2 = (a, b) =>
+        (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
+
+      const sorted = points
+        .filter(p => p !== p0)
+        .sort((a, b) => {
+          const c = cross(p0, a, b);
+          if (c !== 0) return c > 0 ? -1 : 1;
+          return dist2(p0, a) - dist2(p0, b);
+        });
+
+      const hull = [p0];
+      for (const p of sorted) {
+        while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) {
+          hull.pop();
+        }
+        hull.push(p);
+      }
+      return hull;
+    },
+
+    /**
+     * 清除子区圆形蒙层
+     */
+    clearSubAreaOverlays() {
+      if (this.map && this.subAreaOverlays.length > 0) {
+        try { this.map.remove(this.subAreaOverlays); } catch (e) { void e; }
+        this.subAreaOverlays = [];
+      }
+    },
   }
   }
 };
 };
 </script>
 </script>

+ 2 - 0
src/views/StatusMonitoring.vue

@@ -327,6 +327,8 @@ export default {
             const zoom = leaves.length <= 6 ? 15 : 14;
             const zoom = leaves.length <= 6 ? 15 : 14;
             const map = this.$refs.trafficMapRef.map;
             const map = this.$refs.trafficMapRef.map;
             if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
             if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
+            // 绘制该子区的圆形蒙层(自动替换上一个)
+            this.$refs.trafficMapRef.drawSubAreaCircle(leaves);
         },
         },
         // 处理菜单点击
         // 处理菜单点击
         handleMenuClick(nodeData) {
         handleMenuClick(nodeData) {