Pārlūkot izejas kodu

Merge branch 'master' of http://121.40.40.223:3000/zizhong.wang/dtScreen

hebotao 2 nedēļas atpakaļ
vecāks
revīzija
58b2811543

+ 2 - 2
src/api/index.js

@@ -101,8 +101,8 @@ export const apiGetSpecialTaskMonitorData = (id) =>
 export const apiGetCrossingPanelData = (id) =>
   http.get(`/crossing/panel/${id}`)
 
-export const apiGetCrossingDetailData = (id) =>
-  http.get(`/crossing/detail/${id}`)
+export const apiGetCrossingDetailData = (id, { iconMode } = {}) =>
+  http.get(`/crossing/detail/${id}`, { params: iconMode ? { iconMode } : undefined })
 
 export const apiGetCrossingTopCharts = () =>
   http.get('/crossing/top-charts')

+ 75 - 40
src/components/TongzhouTrafficMap.vue

@@ -208,23 +208,25 @@ export default {
     // 将真实路口数据按状态类型分类
     classifyIntersectionsByStatus() {
       const remainingData = this.intersectionData;
-      const normalStatusCount = 6;
-      const abnormalStatusCount = 3;
-      const chunkSize = Math.floor(remainingData.length / (normalStatusCount + abnormalStatusCount));
       const maxAbnormalCount = 4;
+      // 异常状态各取4个,剩余全部按比例分配给6个正常状态
+      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 = {
-        "中心计划": remainingData.slice(0, 20),
+        "中心计划": normalData.slice(0, normalChunk),
         "干线协调": [], // 清空,改为动态生成
         "勤务路线": [], // 清空,改为动态生成
-        "定周期控制": remainingData.slice(chunkSize, chunkSize * 2),
-        "感应控制": remainingData.slice(chunkSize * 2, chunkSize * 3),
-        "自适应控制": remainingData.slice(chunkSize * 3, chunkSize * 4),
-        "手动控制": remainingData.slice(chunkSize * 4, chunkSize * 5),
-        "特殊控制": remainingData.slice(chunkSize * 5, chunkSize * 6),
-        "离线": remainingData.slice(chunkSize * 6, Math.min(chunkSize * 7, chunkSize * 6 + maxAbnormalCount)),
-        "降级": remainingData.slice(chunkSize * 7, Math.min(chunkSize * 8, chunkSize * 7 + maxAbnormalCount)),
-        "故障": remainingData.slice(chunkSize * 8, Math.min(chunkSize * 9, chunkSize * 8 + maxAbnormalCount))
+        "定周期控制": 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)
       };
     },
 
@@ -311,11 +313,12 @@ export default {
 
       const realRouteConfigs = {
         "干线协调": [
-          { start: [116.6421, 39.9272], end: [116.6825, 39.9272], color: "#13C373" },
-          { start: [116.6426, 39.9221], end: [116.6830, 39.9221], color: "#13C373" },
-          { start: [116.6432, 39.9171], end: [116.6833, 39.9171], color: "#13C373" },
-          { start: [116.6438, 39.9123], end: [116.6841, 39.9123], color: "#13C373" },
-          { start: [116.6445, 39.9075], end: [116.6846, 39.9075], color: "#13C373" }
+          { start: [116.6421, 39.9272], end: [116.6623, 39.9272], color: "#13C373", trunkId: 'trunk_1', trunkName: '古城南路与古城大街' },
+          { start: [116.6623, 39.9272], end: [116.6825, 39.9272], color: "#13C373", trunkId: 'trunk_2', trunkName: '古城西路东口南一过街' },
+          { start: [116.6426, 39.9221], end: [116.6628, 39.9221], color: "#13C373", trunkId: 'trunk_3', trunkName: '古城大街与古城北路' },
+          { start: [116.6628, 39.9221], end: [116.683, 39.9221], color: "#13C373", trunkId: 'trunk_4', trunkName: '八角北路与八角东街' },
+          { start: [116.6432, 39.9171], end: [116.66325, 39.9171], color: "#13C373", trunkId: 'trunk_5', trunkName: '古城西路与古城大街' },
+          { start: [116.66325, 39.9171], end: [116.6833, 39.9171], color: "#13C373", trunkId: 'trunk_6', trunkName: '张台路与湖亦路路口' },
         ],
         "勤务路线": [
           // 第一段:未执行(全红)— 通州大街西段,干线协调下方约600m
@@ -336,6 +339,7 @@ export default {
           const markers = intersections.map(item =>
             this.createTrafficLightMarker([item["位置-经度"], item["位置-纬度"]], {
               ...config,
+              id: item["路口编号"],
               road: item["路口名称"] || '规划路口'
             })
           ).filter(Boolean);
@@ -368,25 +372,16 @@ export default {
             path
           });
 
-          // 统计干线协调实际生成的线段数
+          // 每条路线配置对应一条干线菜单项
           if (config.name === '干线协调' && overlays.length > 0) {
-            const trunkNames = [
-              '古城南路与古城大街', '古城西路东口南一过街', '古城大街与古城北路',
-              '八角北路与八角东街', '古城西路与古城大街'
-            ];
-            const polylineCount = overlays.filter(o => o instanceof this.AMap.Polyline).length;
-            for (let s = 0; s < polylineCount; s++) {
-              const idx = trunkSegments.length + 1;
-              const name = trunkNames[idx - 1] || ('干线' + idx);
-              trunkSegments.push({
-                id: 'trunk_' + idx,
-                label: name,
-                intersections: Array.from({ length: 6 }, (_, k) => name + '_路口' + (k + 1)),
-                distances: Array.from({ length: 6 }, (_, k) => k * 1000),
-                _lineIdx: lineIdx,
-                _segmentIdx: s
-              });
-            }
+            const name = line.trunkName || ('干线' + (lineIdx + 1));
+            trunkSegments.push({
+              id: line.trunkId || ('trunk_' + (lineIdx + 1)),
+              label: name,
+              intersections: Array.from({ length: 6 }, (_, k) => name + '_路口' + (k + 1)),
+              distances: Array.from({ length: 6 }, (_, k) => k * 500),
+              _lineIdx: lineIdx
+            });
           }
 
           if (this.isComponentDestroyed || drawSeq !== this.drawSeq) return;
@@ -497,7 +492,11 @@ export default {
         return [lng + offsetVal, lat + offsetVal];
       };
 
-      const segments = this.extractMainStraightSegments(basePath);
+      const allSegments = this.extractMainStraightSegments(basePath);
+      // 干线协调每条路线只取最长的1段,保证6条配置 = 6条线
+      const segments = configName === '干线协调'
+        ? allSegments.slice(0, 1)
+        : allSegments;
       const overlays = [];
 
       segments.forEach((rawSegmentPath, segmentIdx) => {
@@ -690,10 +689,18 @@ export default {
               else if (i === indices.length - 1) markerType = 'end';
             }
 
+            // 干线协调:id 用干线编号,road 用干线名称(与菜单标题一致)
+            const markerId = (configName === '干线协调' && line.trunkId)
+              ? `${line.trunkId}_point_${i + 1}`
+              : `MOCK-${configName.charAt(0)}-${lineIdx}-${segmentIdx}-${idx}`;
+            const markerRoad = (configName === '干线协调' && line.trunkName)
+              ? line.trunkName
+              : `${configName}路口-${lineIdx}-${segmentIdx}-${idx}`;
+
             const marker = this.createTrafficLightMarker([lng, lat], {
               ...config,
-              id: `MOCK-${configName.charAt(0)}-${lineIdx}-${segmentIdx}-${idx}`,
-              road: `${configName}路口-${lineIdx}-${segmentIdx}-${idx}`
+              id: markerId,
+              road: markerRoad
             }, markerType);
             if (marker) overlays.push(marker);
           }
@@ -1135,13 +1142,13 @@ export default {
           const dx = markerPos[0] - targetLng;
           const dy = markerPos[1] - targetLat;
           const distSq = dx * dx + dy * dy;
-          
+
           // 容差范围内(约 20 米),寻找最接近的点
           if (distSq < 0.000001) {
             // 优先规则:如果距离相同或非常接近,优先选择异常状态点(离线/降级/故障)
             const isAbnormal = ["离线", "降级", "故障"].includes(markerExt.name);
             const currentIsAbnormal = bestMarker ? ["离线", "降级", "故障"].includes(bestMarker.getExtData().name) : false;
-            
+
             if (distSq < minDistanceSq || (isAbnormal && !currentIsAbnormal)) {
               minDistanceSq = distSq;
               bestMarker = item;
@@ -1162,6 +1169,34 @@ export default {
       }
     },
 
+    // 通过路口编号定位到对应的 marker,弹出信息窗
+    focusById(id) {
+      if (!this.isMapReady() || !id) return null;
+
+      let bestMarker = null;
+      Object.values(this.routeGroups).forEach(group => {
+        if (!Array.isArray(group) || bestMarker) return;
+        group.forEach(item => {
+          if (bestMarker) return;
+          if (!(item instanceof this.AMap.Marker)) return;
+          const ext = item.getExtData();
+          if (ext.id === id || ext['路口编号'] === id) {
+            bestMarker = item;
+          }
+        });
+      });
+
+      if (bestMarker) {
+        const finalPos = bestMarker.getPosition();
+        this.map.setZoomAndCenter(17, finalPos, false, 500);
+        setTimeout(() => {
+          if (!this.isComponentDestroyed) this.openLightInfo(bestMarker.getExtData(), finalPos);
+        }, 600);
+        return finalPos;
+      }
+      return null;
+    },
+
     toggleLegend() {
       this.legendVisible = !this.legendVisible;
     },

+ 71 - 17
src/components/ui/CrossingDetailPanel.vue

@@ -73,10 +73,10 @@
                                     <div class="donut-title">实时方案(执行方案3)</div>
                                     <PlanDonutChart
                                         :chartData="realtimeDonutData"
-                                        centerValue="94"
+                                        :centerValue="String(realtimeRemaining)"
                                         centerLabel="剩余时长"
                                         :showTotal="true"
-                                        :totalValue="180"
+                                        :totalValue="cycleLength"
                                         :scale="panelScale"
                                     />
                                 </div>
@@ -84,7 +84,7 @@
                                     <div class="donut-title">下周期方案</div>
                                     <PlanDonutChart
                                         :chartData="nextCycleDonutData"
-                                        centerValue="98"
+                                        :centerValue="String(cycleLength)"
                                         centerLabel="总时长"
                                         :showTotal="false"
                                         :scale="panelScale"
@@ -150,7 +150,8 @@ export default {
         PlanDonutChart
     },
     props: {
-        preloadedData: { type: Object, default: null }
+        preloadedData: { type: Object, default: null },
+        iconMode: { type: String, default: 'simple' }  // 'default' | 'simple'
     },
     data() {
         return {
@@ -175,19 +176,14 @@ export default {
             currentMethod: 'temp',
             currentScheme: 'early_peak',
             schemeOptions: [],
-            // 实时方案圆饼图数据
-            realtimeDonutData: [
-                { label: '已走时长', value: 86, color: '#8892a0' },
-                { label: '1-西单面', value: 0, color: '#3b82f6' },
-                { label: '2-东西直行', value: 43, color: '#a855f7' },
-                { label: '3-北左转', value: 51, color: '#14b8a6' }
-            ],
+            // 实时方案圆饼图数据(从 phaseData 动态生成)
+            realtimeDonutData: [],
             // 下周期方案圆饼图数据
-            nextCycleDonutData: [
-                { label: '1-西单面', value: 23, color: '#3b82f6' },
-                { label: '2-东西直行', value: 51, color: '#a855f7' },
-                { label: '3-北左转', value: 24, color: '#14b8a6' }
-            ],
+            nextCycleDonutData: [],
+            // 实时方案剩余时长
+            realtimeRemaining: 0,
+            // 各阶段基础数据(从 phaseData 解析)
+            phaseStages: [],
             currentLocktime: 50,
             locktimeOptions: [],
             currentStage: '1', 
@@ -265,6 +261,63 @@ export default {
                     activeArrowTypes: ewGreen ? activeArrowTypes : []
                 }
             });
+
+            // 更新实时方案圆饼图的已走时长
+            this.updateRealtimeDonut(activeTime);
+        },
+        // 从 phaseData 解析4个阶段的绿灯时长,构建圆饼图数据
+        buildDonutFromPhaseData() {
+            const phaseData = this.mockPhaseData || [];
+            const stageColors = ['#3b82f6', '#a855f7', '#14b8a6', '#f59e0b'];
+            const stageLabels = ['1-南北直行', '2-南北左转', '3-东西直行', '4-东西左转'];
+
+            // 提取 track 0 的绿灯相位(每阶段的第一个 green)
+            const greenPhases = phaseData.filter(p => p[0] === 0 && p[5] === 'green');
+            const stages = greenPhases.slice(0, 4).map((p, i) => ({
+                label: stageLabels[i] || `阶段${i + 1}`,
+                value: p[4],  // 绿灯时长
+                start: p[1],  // 阶段开始时间
+                end: p[2],    // 绿灯结束时间
+                color: stageColors[i]
+            }));
+            this.phaseStages = stages;
+
+            // 实时方案:已走时长 + 4个阶段
+            this.realtimeDonutData = [
+                { label: '已走时长', value: 0, color: '#8892a0' },
+                ...stages.map(s => ({ label: s.label, value: s.value, color: s.color }))
+            ];
+            this.realtimeRemaining = this.cycleLength;
+
+            // 下周期方案:4个阶段,时长随机微调 ±3s
+            this.nextCycleDonutData = stages.map(s => ({
+                label: s.label,
+                value: Math.max(10, s.value + Math.floor(Math.random() * 7) - 3),
+                color: s.color
+            }));
+        },
+        // 根据扫描线时间更新实时方案圆饼图
+        updateRealtimeDonut(activeTime) {
+            if (this.phaseStages.length === 0) return;
+            const elapsed = Math.round(activeTime);
+            const remaining = Math.max(0, this.cycleLength - elapsed);
+            this.realtimeRemaining = remaining;
+
+            // 计算各阶段的剩余时长
+            const stageValues = this.phaseStages.map(s => {
+                if (activeTime >= s.end) return 0;            // 阶段已完成
+                if (activeTime < s.start) return s.value;     // 阶段未开始
+                return Math.max(0, Math.round(s.end - activeTime)); // 阶段进行中
+            });
+
+            this.realtimeDonutData = [
+                { label: '已走时长', value: elapsed, color: '#8892a0' },
+                ...this.phaseStages.map((s, i) => ({
+                    label: s.label,
+                    value: stageValues[i],
+                    color: s.color
+                }))
+            ];
         },
         initScaleObserver() {
             const ro = new ResizeObserver(entries => {
@@ -278,7 +331,7 @@ export default {
         },
         async loadData() {
             const nodeId = this.$attrs.id || this.id;
-            const data = await apiGetCrossingDetailData(nodeId);
+            const data = await apiGetCrossingDetailData(nodeId, { iconMode: this.iconMode });
             if (data) {
                 this.applyData(data);
             }
@@ -292,6 +345,7 @@ export default {
             this.phaseDiff = data.phaseDiff || 0;
             this.coordTime = data.coordTime || 0;
             this.currentStageList = data.stageList || [];
+            this.buildDonutFromPhaseData();
             this.$nextTick(() => {
                 this.dataReady = true;
             });

+ 1 - 1
src/components/ui/CrossingMultiView.vue

@@ -156,7 +156,7 @@ export default {
             if (needLoad.length === 0) return;
             const version = ++this.rebuildVersion;
             const results = await Promise.all(
-                needLoad.map(item => apiGetCrossingDetailData(item.id).catch(() => null))
+                needLoad.map(item => apiGetCrossingDetailData(item.id, { iconMode: 'simple' }).catch(() => null))
             );
             if (version !== this.rebuildVersion) return;
             results.forEach((data, i) => {

+ 42 - 22
src/mock/api.js

@@ -110,7 +110,7 @@ function _camerasToArmTypes(cameras) {
   return result
 }
 
-function _makeIntersectionConfig(id, name, { fixedNsGreen } = {}) {
+function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default' } = {}) {
   const phases = ['南北直行', '东西直行', '北单放', '东单放']
   const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false
   const countdown = 10 + Math.floor(Math.random() * 50)
@@ -119,10 +119,21 @@ function _makeIntersectionConfig(id, name, { fixedNsGreen } = {}) {
   const cameras = _makeCameras(id, name, seed)
   const armCamTypes = _camerasToArmTypes(cameras)
 
-  const lanePresets = [
-    ['U', 'L', 'S', 'R'], ['U', 'L', 'S', 'R'],
-    ['U', 'L', 'S', 'R'], ['U', 'L', 'S', 'R'],
-  ]
+  const lanesPresetMap = {
+    default: {
+      N: ['U', 'L', 'S', 'R'],
+      S: ['U', 'L', 'S', 'R'],
+      E: ['U', 'L', 'S', 'R'],
+      W: ['U', 'L', 'S', 'R'],
+    },
+    simple: {
+      N: ['L', 'S', null, null],  // 北:左转+直行
+      S: ['U', 'S', null, null],  // 南:掉头+直行
+      E: ['L', 'S', null, null],  // 东:掉头+直行
+      W: ['U', 'S', null, null],  // 西:左转+直行
+    },
+  }
+  const lanesPreset = lanesPresetMap[iconMode] || lanesPresetMap.default
 
   return {
     status: _getDeviceStatus(id),
@@ -131,10 +142,10 @@ function _makeIntersectionConfig(id, name, { fixedNsGreen } = {}) {
       ew: { phaseName: phases[1], time: countdown, isGreen: !nsGreen },
     },
     armsConfig: {
-      N: { cameraType: armCamTypes.N, lanes: lanePresets[0] },
-      S: { cameraType: armCamTypes.S, lanes: lanePresets[1] },
-      E: { cameraType: armCamTypes.E, lanes: lanePresets[2] },
-      W: { cameraType: armCamTypes.W, lanes: lanePresets[3] },
+      N: { cameraType: armCamTypes.N, lanes: lanesPreset.N },
+      S: { cameraType: armCamTypes.S, lanes: lanesPreset.S },
+      E: { cameraType: armCamTypes.E, lanes: lanesPreset.E },
+      W: { cameraType: armCamTypes.W, lanes: lanesPreset.W },
     },
     cameras,
   }
@@ -145,9 +156,9 @@ function _makeIntersectionConfig(id, name, { fixedNsGreen } = {}) {
  * @param {number} cycleLength 周期总时长
  * @param {boolean} isTwoRows 是否生成上下双排 8 相位 (默认 true)
  */
-function _makePhaseData(cycleLength = 140, isTwoRows = true) {
+function _makePhaseData(cycleLength = 140, isTwoRows = true, iconMode = 'default') {
   const n = 4; // 4个阶段 (S1-S4)
-  const stageTime = Math.floor(cycleLength / n); 
+  const stageTime = Math.floor(cycleLength / n);
   const pd = [];
 
   // ==========================================
@@ -155,12 +166,21 @@ function _makePhaseData(cycleLength = 140, isTwoRows = true) {
   // 前端组件会按逗号切割并分别放到对角位置
   // ==========================================
   // 固定4个阶段的图标和方向:P1南北直行、P2南北左转、P3东西直行、P4东西左转
-  const phaseConfig = [
-    { icon: 'STRAIGHT_DOWN,STRAIGHT_UP', direction: 'ns' },       // P1: 南北直行
-    { icon: 'TURN_DOWN_LEFT,TURN_UP_LEFT', direction: 'ns' },     // P2: 南北左转
-    { icon: 'STRAIGHT_LEFT,STRAIGHT_RIGHT', direction: 'ew' },    // P3: 东西直行
-    { icon: 'TURN_LEFT_DOWN,TURN_RIGHT_UP', direction: 'ew' },    // P4: 东西左转
-  ];
+  const phaseConfigMap = {
+    default: [
+      { icon: 'STRAIGHT_DOWN,STRAIGHT_UP', direction: 'ns' },       // P1: 南北直行
+      { icon: 'TURN_DOWN_LEFT,TURN_UP_LEFT', direction: 'ns' },     // P2: 南北左转
+      { icon: 'STRAIGHT_LEFT,STRAIGHT_RIGHT', direction: 'ew' },    // P3: 东西直行
+      { icon: 'TURN_LEFT_DOWN,TURN_RIGHT_UP', direction: 'ew' },    // P4: 东西左转
+    ],
+    simple: [
+      { icon: 'STRAIGHT_DOWN,STRAIGHT_UP', direction: 'ns' },              // P1: 南北直行
+      { icon: 'TURN_DOWN_LEFT,TURN_UP_LEFT_UTURN', direction: 'ns' },      // P2: 北左转+南掉头
+      { icon: 'STRAIGHT_LEFT,STRAIGHT_RIGHT', direction: 'ew' },           // P3: 东西直行
+      { icon: 'TURN_LEFT_DOWN,TURN_RIGHT_UP_UTURN', direction: 'ew' },      // P4: 东左转+西掉头
+    ],
+  };
+  const phaseConfig = phaseConfigMap[iconMode] || phaseConfigMap.default;
 
   let t = 0;
   for (let i = 0; i < n; i++) {
@@ -786,7 +806,7 @@ export async function apiGetSpecialTaskMonitorData(id, { fixedNsGreen } = {}) {
 export async function apiGetCrossingPanelData(id) {
   await delay(300)
   const point = DB.points.find(p => p.id === id)
-  const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false })
+  const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false, iconMode: 'simple' })
   const seed = id ? id.charCodeAt(id.length - 1) : 0
   // 确保 config 有 status
   if (!config.status) {
@@ -795,7 +815,7 @@ export async function apiGetCrossingPanelData(id) {
 
   const preset = DB.signalTimings[id]
   const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][Math.abs(seed) % 6]
-  const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false)
+  const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false, 'simple')
   const currentTime = Math.floor(Date.now() / 1000) % cycleLength
 
   return ok({
@@ -814,10 +834,10 @@ export async function apiGetCrossingPanelData(id) {
 /**
  * GET /api/crossing/detail/:id — CrossingDetailPanel 弹窗
  */
-export async function apiGetCrossingDetailData(id) {
+export async function apiGetCrossingDetailData(id, { iconMode = 'default' } = {}) {
   await delay(350)
   const point = DB.points.find(p => p.id === id)
-  const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false })
+  const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false, iconMode })
 
   // 用 id 的全部字符生成稳定 seed(加权位置避免 charCode 总和碰撞)
   const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
@@ -830,7 +850,7 @@ export async function apiGetCrossingDetailData(id) {
   // 从真实阶段数据推导周期和相位
   const preset = DB.signalTimings[id]
   const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][seed % 6]
-  const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength || 140, false)
+  const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength || 140, false, iconMode)
 
   // 从相位数据中提取阶段列表(优先上轨道绿灯相位,单轨道时取 track 0,最多4个)
   const hasTrack1 = phaseData.some(p => p[0] === 1)

+ 2 - 1
src/mock/mockAdapter.js

@@ -191,7 +191,8 @@ mock.onGet(/\/crossing\/panel\/(.+)/).reply(async (config) => {
 
 mock.onGet(/\/crossing\/detail\/(.+)/).reply(async (config) => {
   const id = config.url.match(/\/crossing\/detail\/(.+)/)[1]
-  const res = await mockApi.apiGetCrossingDetailData(id)
+  const iconMode = (config.params && config.params.iconMode) || 'default'
+  const res = await mockApi.apiGetCrossingDetailData(id, { iconMode })
   return [200, res]
 })
 

+ 1 - 175
src/mock/mock_data.json

@@ -30015,7 +30015,7 @@
                 },
                 {
                   "id": "trunk_6",
-                  "label": "干线6",
+                  "label": "张台路与湖亦路路口",
                   "intersections": [
                     "干线6_路口1",
                     "干线6_路口2",
@@ -30070,180 +30070,6 @@
                       "lat": 39.9171
                     }
                   ]
-                },
-                {
-                  "id": "trunk_7",
-                  "label": "干线7",
-                  "intersections": [
-                    "干线7_路口1",
-                    "干线7_路口2",
-                    "干线7_路口3",
-                    "干线7_路口4",
-                    "干线7_路口5",
-                    "干线7_路口6"
-                  ],
-                  "distances": [
-                    0,
-                    500,
-                    1000,
-                    1500,
-                    2000,
-                    2500
-                  ],
-                  "points": [
-                    {
-                      "id": "GX007001",
-                      "label": "干线7_路口1",
-                      "lng": 116.6438,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX007002",
-                      "label": "干线7_路口2",
-                      "lng": 116.64783,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX007003",
-                      "label": "干线7_路口3",
-                      "lng": 116.65186,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX007004",
-                      "label": "干线7_路口4",
-                      "lng": 116.65589,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX007005",
-                      "label": "干线7_路口5",
-                      "lng": 116.65992,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX007006",
-                      "label": "干线7_路口6",
-                      "lng": 116.66395,
-                      "lat": 39.9123
-                    }
-                  ]
-                },
-                {
-                  "id": "trunk_8",
-                  "label": "干线8",
-                  "intersections": [
-                    "干线8_路口1",
-                    "干线8_路口2",
-                    "干线8_路口3",
-                    "干线8_路口4",
-                    "干线8_路口5",
-                    "干线8_路口6"
-                  ],
-                  "distances": [
-                    0,
-                    500,
-                    1000,
-                    1500,
-                    2000,
-                    2500
-                  ],
-                  "points": [
-                    {
-                      "id": "GX008001",
-                      "label": "干线8_路口1",
-                      "lng": 116.66395,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX008002",
-                      "label": "干线8_路口2",
-                      "lng": 116.66798,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX008003",
-                      "label": "干线8_路口3",
-                      "lng": 116.67201,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX008004",
-                      "label": "干线8_路口4",
-                      "lng": 116.67604,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX008005",
-                      "label": "干线8_路口5",
-                      "lng": 116.68007,
-                      "lat": 39.9123
-                    },
-                    {
-                      "id": "GX008006",
-                      "label": "干线8_路口6",
-                      "lng": 116.6841,
-                      "lat": 39.9123
-                    }
-                  ]
-                },
-                {
-                  "id": "trunk_9",
-                  "label": "干线9",
-                  "intersections": [
-                    "干线9_路口1",
-                    "干线9_路口2",
-                    "干线9_路口3",
-                    "干线9_路口4",
-                    "干线9_路口5",
-                    "干线9_路口6"
-                  ],
-                  "distances": [
-                    0,
-                    500,
-                    1000,
-                    1500,
-                    2000,
-                    2500
-                  ],
-                  "points": [
-                    {
-                      "id": "GX009001",
-                      "label": "干线9_路口1",
-                      "lng": 116.6445,
-                      "lat": 39.9075
-                    },
-                    {
-                      "id": "GX009002",
-                      "label": "干线9_路口2",
-                      "lng": 116.65252,
-                      "lat": 39.9075
-                    },
-                    {
-                      "id": "GX009003",
-                      "label": "干线9_路口3",
-                      "lng": 116.66054,
-                      "lat": 39.9075
-                    },
-                    {
-                      "id": "GX009004",
-                      "label": "干线9_路口4",
-                      "lng": 116.66856,
-                      "lat": 39.9075
-                    },
-                    {
-                      "id": "GX009005",
-                      "label": "干线9_路口5",
-                      "lng": 116.67658,
-                      "lat": 39.9075
-                    },
-                    {
-                      "id": "GX009006",
-                      "label": "干线9_路口6",
-                      "lng": 116.6846,
-                      "lat": 39.9075
-                    }
-                  ]
                 }
               ]
             }

+ 22 - 13
src/views/SpecialSituationMonitoring.vue

@@ -234,7 +234,7 @@ export default {
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
             // 干线marker点击时,从菜单数据中匹配对应干线
-            if (this.activeLeftTab === 'trunkLine' && mapData.id && String(mapData.id).startsWith('MOCK-干')) {
+            if (this.activeLeftTab === 'trunkLine' && mapData.id) {
                 const matched = this.findTrunkMenuNode(mapData.id);
                 if (matched) {
                     nodeData.id = matched.id;
@@ -295,15 +295,25 @@ export default {
             this.crossingSelections = [];
             this.showTopChartDalogs(); // 根据当前Tab显示对应的顶部常驻图表
         },
-        // 处理菜单folder标题点击
+        // 处理菜单folder标题点击:计算子区路口中心坐标,移动地图
         handleFolderClick(nodeData) {
             console.log('父组件接收到了文件夹点击事件:', nodeData);
-            // 临时逻辑,有真实接口后可以删除
-            const index = Math.floor(Math.random() * 10);
-            const position = localStorage.getItem(`pos${index + 1}`).split(',');
-
-            // 地图联动
-            this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
+            const leaves = [];
+            const collect = (nodes) => {
+                if (!Array.isArray(nodes)) return;
+                for (const n of nodes) {
+                    if (n.children) collect(n.children);
+                    else if (n.lng && n.lat) leaves.push(n);
+                }
+            };
+            collect(nodeData.children || []);
+            if (leaves.length === 0 || !this.$refs.trafficMapRef) return;
+
+            const avgLng = leaves.reduce((s, n) => s + n.lng, 0) / leaves.length;
+            const avgLat = leaves.reduce((s, n) => s + n.lat, 0) / leaves.length;
+            const zoom = leaves.length <= 6 ? 15 : 14;
+            const map = this.$refs.trafficMapRef.map;
+            if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
         },
         // 处理菜单点击
         handleMenuClick(nodeData) {
@@ -442,7 +452,7 @@ export default {
         // 单个路口详情弹窗(总览双击展开等场景使用)
         async showCrossingDetailDialogs(nodeData) {
             console.log('显示路口详情弹窗组', nodeData.id, nodeData.label);
-            const detailData = await apiGetCrossingDetailData(nodeData.id);
+            const detailData = await apiGetCrossingDetailData(nodeData.id, { iconMode: 'simple' });
             const dialogId = 'crossing_detail' + nodeData.id;
             this.$refs.layout.openDialog({
                 id: dialogId,
@@ -476,9 +486,8 @@ export default {
             this.showTrunkLineDalogs(nodeData);
         },
         findTrunkMenuNode(markerId) {
-            const parts = markerId.split('-');
-            const lineIdx = parseInt(parts[2], 10);
-            const segmentIdx = parseInt(parts[3], 10);
+            // 从 marker ID(如 trunk_1_point_3)提取干线编号(trunk_1)
+            const trunkId = String(markerId).replace(/_point_\d+$/, '');
             const leaves = [];
             const walk = (nodes) => {
                 if (!Array.isArray(nodes)) return;
@@ -488,7 +497,7 @@ export default {
                 }
             };
             walk(this.trunkLineMenuData);
-            return leaves.find(n => n._lineIdx === lineIdx && n._segmentIdx === segmentIdx) || null;
+            return leaves.find(n => n.id === trunkId) || null;
         },
         handleTrunkMenuUpdate(segments) {
             this.trunkLineMenuData = [{

+ 64 - 36
src/views/StatusMonitoring.vue

@@ -240,7 +240,7 @@ export default {
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
             // 干线marker点击时,从菜单数据中匹配对应干线
-            if (this.activeLeftTab === 'trunkLine' && mapData.id && String(mapData.id).startsWith('MOCK-干')) {
+            if (this.activeLeftTab === 'trunkLine' && mapData.id) {
                 const matched = this.findTrunkMenuNode(mapData.id);
                 if (matched) {
                     nodeData.id = matched.id;
@@ -301,41 +301,68 @@ export default {
             this.crossingSelections = [];
             this.showTopChartDalogs(); // 根据当前Tab显示对应的顶部常驻图表
         },
-        // 处理菜单folder标题点击
+        // 处理菜单folder标题点击:计算子区路口中心坐标,移动地图
         handleFolderClick(nodeData) {
             console.log('父组件接收到了文件夹点击事件:', nodeData);
-            // 临时逻辑,有真实接口后可以删除
-            const index = Math.floor(Math.random() * 10);
-            const position = localStorage.getItem(`pos${index + 1}`).split(',');
-
-            // 地图联动
-            this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
+            const leaves = [];
+            const collect = (nodes) => {
+                if (!Array.isArray(nodes)) return;
+                for (const n of nodes) {
+                    if (n.children) collect(n.children);
+                    else if (n.lng && n.lat) leaves.push(n);
+                }
+            };
+            collect(nodeData.children || []);
+            if (leaves.length === 0 || !this.$refs.trafficMapRef) return;
+
+            const avgLng = leaves.reduce((s, n) => s + n.lng, 0) / leaves.length;
+            const avgLat = leaves.reduce((s, n) => s + n.lat, 0) / leaves.length;
+            const zoom = leaves.length <= 6 ? 15 : 14;
+            const map = this.$refs.trafficMapRef.map;
+            if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
         },
         // 处理菜单点击
         handleMenuClick(nodeData) {
             console.log('父组件接收到了最底层路口点击事件:', nodeData);
-            // 通过地图组件获取像素坐标(如果有经纬度的话)
-            // if (nodeData.lng && nodeData.lat && this.$refs.trafficMapRef) {
-            //     // 地图联动
-            //     this.$refs.trafficMapRef.focusByLocation([nodeData.lng, nodeData.lat]);
-
-            //     const pixel = this.$refs.trafficMapRef.lngLatToPixel(nodeData.lng, nodeData.lat);
-            //     if (pixel) {
-            //         const scale = window.innerWidth / 1920;
-            //         nodeData.pixelX = Math.round(pixel.x / scale) + 20;
-            //         nodeData.pixelY = Math.round(pixel.y / scale);
-            //     }
-            // }
-
-            // 根据Tab来显示不同的弹窗内容
-            if (this.activeLeftTab === 'overview') { // 总览
-                // 临时逻辑,有真实接口后可以删除
-                const index = Math.floor(Math.random() * 10);
-                const position = localStorage.getItem(`pos${index + 1}`).split(',');
 
-                // 地图联动
-                this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
-                
+            const showDialog = () => {
+                if (this.activeLeftTab === 'overview') {
+                    this.showOverviewDalogs(nodeData);
+                } else if (this.activeLeftTab === 'crossing') {
+                    this.showCrossingDalogs(nodeData);
+                } else if (this.activeLeftTab === 'trunkLine') {
+                    this.showTrunkLineDalogs(nodeData);
+                } else if (this.activeLeftTab === 'specialDuty') {
+                    this.showSpecialDutyDalogs(nodeData);
+                }
+            };
+
+            // 先关闭上一个总览弹窗
+            if (this._lastOverviewDialogId) {
+                this.$refs.layout.handleDialogClose(this._lastOverviewDialogId);
+                this._lastOverviewDialogId = null;
+            }
+
+            // 通过路口编号定位地图 marker,动画完成后计算像素位置再弹窗
+            if (this.$refs.trafficMapRef) {
+                const mapPos = this.$refs.trafficMapRef.focusById(nodeData.id);
+                if (mapPos) {
+                    const lngLat = mapPos.getLng ? [mapPos.getLng(), mapPos.getLat()] : mapPos;
+                    setTimeout(() => {
+                        const pixel = this.$refs.trafficMapRef.lngLatToPixel(lngLat[0], lngLat[1]);
+                        if (pixel) {
+                            const scale = window.innerWidth / 1920;
+                            nodeData.pixelX = Math.round(pixel.x / scale) + 20;
+                            nodeData.pixelY = Math.round(pixel.y / scale);
+                        }
+                        showDialog();
+                    }, 600);
+                    return;
+                }
+            }
+
+            // fallback: 没有地图或找不到 marker 时直接弹窗
+            if (this.activeLeftTab === 'overview') { // 总览
                 this.showOverviewDalogs(nodeData);
             } else if (this.activeLeftTab === 'crossing') { // 路口
                 this.showCrossingDalogs(nodeData);
@@ -361,9 +388,11 @@ export default {
         // 显示总览弹窗组
         showOverviewDalogs(nodeData) {
             console.log('显示总览弹窗组', nodeData.id, nodeData.label);
+            const dialogId = 'crossing3_' + nodeData.id;
+            this._lastOverviewDialogId = dialogId;
             // 路口弹窗
             this.$refs.layout.openDialog({
-                id: 'crossing3_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
+                id: dialogId, // 这里的 ID 可以根据实际业务场景动态生成
                 title: nodeData.label,
                 component: 'CrossingPanel',
                 width: 260,
@@ -392,7 +421,7 @@ export default {
             console.log('路口多选', nodeData.id, nodeData.label);
 
             // 0. 离线检查
-            const detailData = await apiGetCrossingDetailData(nodeData.id);
+            const detailData = await apiGetCrossingDetailData(nodeData.id, { iconMode: 'simple' });
             if (detailData?.intersectionData?.status !== '在线') {
                 this.$msg({
                     title: '提示',
@@ -459,7 +488,7 @@ export default {
         // 单个路口详情弹窗(总览双击展开等场景使用)
         async showCrossingDetailDialogs(nodeData) {
             console.log('显示路口详情弹窗组', nodeData.id, nodeData.label);
-            const detailData = await apiGetCrossingDetailData(nodeData.id);
+            const detailData = await apiGetCrossingDetailData(nodeData.id, { iconMode: 'simple' });
             if (detailData?.intersectionData?.status !== '在线') {
                 this.$msg({
                     title: '提示',
@@ -501,9 +530,8 @@ export default {
             this.showTrunkLineDalogs(nodeData);
         },
         findTrunkMenuNode(markerId) {
-            const parts = markerId.split('-');
-            const lineIdx = parseInt(parts[2], 10);
-            const segmentIdx = parseInt(parts[3], 10);
+            // 从 marker ID(如 trunk_1_point_3)提取干线编号(trunk_1)
+            const trunkId = String(markerId).replace(/_point_\d+$/, '');
             const leaves = [];
             const walk = (nodes) => {
                 if (!Array.isArray(nodes)) return;
@@ -513,7 +541,7 @@ export default {
                 }
             };
             walk(this.trunkLineMenuData);
-            return leaves.find(n => n._lineIdx === lineIdx && n._segmentIdx === segmentIdx) || null;
+            return leaves.find(n => n.id === trunkId) || null;
         },
         handleTrunkMenuUpdate(segments) {
             this.trunkLineMenuData = [{

+ 22 - 15
src/views/TrunkCoordination.vue

@@ -234,7 +234,7 @@ export default {
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
             // 干线marker点击时,从菜单数据中匹配对应干线
-            if (this.activeLeftTab === 'trunkLine' && mapData.id && String(mapData.id).startsWith('MOCK-干')) {
+            if (this.activeLeftTab === 'trunkLine' && mapData.id) {
                 const matched = this.findTrunkMenuNode(mapData.id);
                 if (matched) {
                     nodeData.id = matched.id;
@@ -295,15 +295,25 @@ export default {
             this.crossingSelections = [];
             this.showTopChartDalogs(); // 根据当前Tab显示对应的顶部常驻图表
         },
-        // 处理菜单folder标题点击
+        // 处理菜单folder标题点击:计算子区路口中心坐标,移动地图
         handleFolderClick(nodeData) {
             console.log('父组件接收到了文件夹点击事件:', nodeData);
-            // 临时逻辑,有真实接口后可以删除
-            const index = Math.floor(Math.random() * 10);
-            const position = localStorage.getItem(`pos${index + 1}`).split(',');
-
-            // 地图联动
-            this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
+            const leaves = [];
+            const collect = (nodes) => {
+                if (!Array.isArray(nodes)) return;
+                for (const n of nodes) {
+                    if (n.children) collect(n.children);
+                    else if (n.lng && n.lat) leaves.push(n);
+                }
+            };
+            collect(nodeData.children || []);
+            if (leaves.length === 0 || !this.$refs.trafficMapRef) return;
+
+            const avgLng = leaves.reduce((s, n) => s + n.lng, 0) / leaves.length;
+            const avgLat = leaves.reduce((s, n) => s + n.lat, 0) / leaves.length;
+            const zoom = leaves.length <= 6 ? 15 : 14;
+            const map = this.$refs.trafficMapRef.map;
+            if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
         },
         // 处理菜单点击
         handleMenuClick(nodeData) {
@@ -442,7 +452,7 @@ export default {
         // 单个路口详情弹窗(总览双击展开等场景使用)
         async showCrossingDetailDialogs(nodeData) {
             console.log('显示路口详情弹窗组', nodeData.id, nodeData.label);
-            const detailData = await apiGetCrossingDetailData(nodeData.id);
+            const detailData = await apiGetCrossingDetailData(nodeData.id, { iconMode: 'simple' });
             const dialogId = 'crossing_detail' + nodeData.id;
             this.$refs.layout.openDialog({
                 id: dialogId,
@@ -476,11 +486,8 @@ export default {
             this.showTrunkLineDalogs(nodeData);
         },
         findTrunkMenuNode(markerId) {
-            // markerId 格式: MOCK-干-{lineIdx}-{segmentIdx}-{idx}
-            const parts = markerId.split('-');
-            const lineIdx = parseInt(parts[2], 10);
-            const segmentIdx = parseInt(parts[3], 10);
-            // 从菜单叶子节点中找 _lineIdx 和 _segmentIdx 匹配的
+            // 从 marker ID(如 trunk_1_point_3)提取干线编号(trunk_1)
+            const trunkId = String(markerId).replace(/_point_\d+$/, '');
             const leaves = [];
             const walk = (nodes) => {
                 if (!Array.isArray(nodes)) return;
@@ -490,7 +497,7 @@ export default {
                 }
             };
             walk(this.trunkLineMenuData);
-            return leaves.find(n => n._lineIdx === lineIdx && n._segmentIdx === segmentIdx) || null;
+            return leaves.find(n => n.id === trunkId) || null;
         },
         handleTrunkMenuUpdate(segments) {
             this.trunkLineMenuData = [{