ソースを参照

feat: 地图「勤务路线」增加状态显示

sequoia tungfang 2 週間 前
コミット
186dca58af
共有1 個のファイルを変更した200 個の追加84 個の削除を含む
  1. 200 84
      src/components/TongzhouTrafficMap.vue

+ 200 - 84
src/components/TongzhouTrafficMap.vue

@@ -318,10 +318,12 @@ export default {
           { start: [116.6445, 39.9075], end: [116.6846, 39.9075], color: "#13C373" }
         ],
         "勤务路线": [
-          // G1高速(京哈高速)通州段西半段:通州西入口 → 通州北关立交
-          { start: [116.6320, 39.9375], end: [116.7050, 39.9310], color: "#BC301D" },
-          // G1高速(京哈高速)通州段东半段:通州北关立交 → 通州东出口
-          { start: [116.7050, 39.9310], end: [116.7850, 39.9215], color: "#BC301D" }
+          // 第一段:未执行(全红)
+          { start: [116.6320, 39.9375], end: [116.7050, 39.9310], color: "#BC301D", dutyState: 'pending' },
+          // 第二段:执行中(进度45%,前段置灰后段红色)
+          { start: [116.7050, 39.9310], end: [116.7850, 39.9215], color: "#BC301D", dutyState: 'active', progress: 0.45 },
+          // 第三段:已执行完(全灰)
+          { start: [116.7850, 39.9215], end: [116.8350, 39.9100], color: "#BC301D", dutyState: 'done' }
         ]
       };
 
@@ -504,104 +506,200 @@ export default {
         // 应用偏移到当前段的所有点
         const segmentPath = rawSegmentPath.map(p => applyOffset(p));
 
-        const polyline = new this.AMap.Polyline({
-          path: segmentPath,
-          strokeColor: line.color,
-          strokeWeight: 6,
-          strokeOpacity: 0.8,
-          zIndex: 15
-        });
-        overlays.push(polyline);
-
-        const totalPoints = segmentPath.length;
-
-        // --- 核心优化:全路段物理距离均匀放置方向箭头 ---
-        // 1. 计算整条 segmentPath 的总物理距离及每个点的累计距离
-        const pathDistances = [0];
-        let totalPathDist = 0;
-        for (let j = 0; j < totalPoints - 1; j++) {
-          const d = this.calcApproxDistance(segmentPath[j], segmentPath[j+1]);
-          totalPathDist += d;
-          pathDistances.push(totalPathDist);
-        }
-
-        // 2. 设定标准间距:约每 0.0018 度 (约 200 米) 放置一个箭头;勤务路线密度减半
-        if (configName !== '干线协调') {
-          const targetSpacing = configName === '勤务路线' ? 0.0036 : 0.0018;
-          let currentTargetDist = targetSpacing / 2; // 第一个箭头放在 1/2 间距处,让分布更美观
-
-          while (currentTargetDist < totalPathDist) {
-            // 3. 寻找对应 targetDist 的路径位置 (线性插值)
-            let foundIdx = 0;
-            for (let j = 0; j < pathDistances.length - 1; j++) {
-              if (currentTargetDist >= pathDistances[j] && currentTargetDist <= pathDistances[j + 1]) {
-                foundIdx = j;
-                break;
-              }
+        // --- 勤务路线:根据 line.dutyState 决定渲染样式 ---
+        if (configName === '勤务路线' && line.dutyState) {
+          const state = line.dutyState; // 'pending' | 'active' | 'done'
+          const progress = (state === 'active') ? Math.min(Math.max(Number(line.progress) || 0.5, 0), 1) : (state === 'done' ? 1 : 0);
+
+          // 计算整段累计距离
+          const dists = [0];
+          let totalDist = 0;
+          for (let j = 0; j < segmentPath.length - 1; j++) {
+            totalDist += this.calcApproxDistance(segmentPath[j], segmentPath[j + 1]);
+            dists.push(totalDist);
+          }
+          const splitDist = totalDist * progress;
+
+          // 找切割点坐标(线性插值)
+          let splitIdx = segmentPath.length - 1;
+          let splitPoint = segmentPath[segmentPath.length - 1];
+          for (let j = 0; j < dists.length - 1; j++) {
+            if (splitDist >= dists[j] && splitDist <= dists[j + 1]) {
+              const ratio = dists[j + 1] === dists[j] ? 0 : (splitDist - dists[j]) / (dists[j + 1] - dists[j]);
+              const p1 = segmentPath[j], p2 = segmentPath[j + 1];
+              splitPoint = [
+                Number(p1[0]) + (Number(p2[0]) - Number(p1[0])) * ratio,
+                Number(p1[1]) + (Number(p2[1]) - Number(p1[1])) * ratio
+              ];
+              splitIdx = j;
+              break;
             }
+          }
 
-            const p1 = segmentPath[foundIdx];
-            const p2 = segmentPath[foundIdx + 1];
-            if (p1 && p2) {
-              // 在 p1 和 p2 之间线性插值
-              const ratio = (currentTargetDist - pathDistances[foundIdx]) / (pathDistances[foundIdx + 1] - pathDistances[foundIdx]);
-              const lng1 = Number(p1[0]);
-              const lat1 = Number(p1[1]);
-              const lng2 = Number(p2[0]);
-              const lat2 = Number(p2[1]);
+          // 已过段(灰色)
+          const passedPath = [...segmentPath.slice(0, splitIdx + 1), splitPoint];
+          if (passedPath.length >= 2) {
+            overlays.push(new this.AMap.Polyline({
+              path: passedPath,
+              strokeColor: '#5A5A5A',
+              strokeWeight: 6,
+              strokeOpacity: 0.45,
+              zIndex: 15
+            }));
+          }
 
-              const midLng = lng1 + (lng2 - lng1) * ratio;
-              const midLat = lat1 + (lat2 - lat1) * ratio;
+          // 未过段(原红色),done 状态不绘制
+          const remainPath = [splitPoint, ...segmentPath.slice(splitIdx + 1)];
+          if (state !== 'done' && remainPath.length >= 2) {
+            overlays.push(new this.AMap.Polyline({
+              path: remainPath,
+              strokeColor: line.color,
+              strokeWeight: 6,
+              strokeOpacity: 0.8,
+              zIndex: 15
+            }));
+          }
 
-              const bearing = this.calcBearingDeg(p1, p2);
-              const rotation = bearing - 90;
+          // 执行中:进度点脉冲 marker
+          if (state === 'active') {
+            overlays.push(new this.AMap.Marker({
+              position: splitPoint,
+              zIndex: 150,
+              offset: new this.AMap.Pixel(-12, -12),
+              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>`
+            }));
+          }
 
-              const directionMarker = new this.AMap.Marker({
+          // 方向箭头:已过段灰色低透明,未过段正常
+          const targetSpacing = 0.0036;
+          let arrowDist = targetSpacing / 2;
+          while (arrowDist < totalDist) {
+            let foundIdx = 0;
+            for (let j = 0; j < dists.length - 1; j++) {
+              if (arrowDist >= dists[j] && arrowDist <= dists[j + 1]) { foundIdx = j; break; }
+            }
+            const p1 = segmentPath[foundIdx], p2 = segmentPath[foundIdx + 1];
+            if (p1 && p2) {
+              const ratio = (arrowDist - dists[foundIdx]) / (dists[foundIdx + 1] - dists[foundIdx]);
+              const midLng = Number(p1[0]) + (Number(p2[0]) - Number(p1[0])) * ratio;
+              const midLat = Number(p1[1]) + (Number(p2[1]) - Number(p1[1])) * ratio;
+              const isPassed = arrowDist <= splitDist;
+              const rotation = this.calcBearingDeg(p1, p2) - 90;
+              overlays.push(new this.AMap.Marker({
                 position: [midLng, midLat],
-                content: `
-                  <div style="transform: rotate(${rotation}deg); width: 20px; height: 10px; display: flex; align-items: center; pointer-events: none; opacity: 0.85;">
-                    <img src="${require('@/assets/map/direction.png')}" style="width: 100%; height: auto;" />
-                  </div>
-                `,
+                content: `<div style="transform:rotate(${rotation}deg);width:20px;height:10px;display:flex;align-items:center;pointer-events:none;opacity:${isPassed ? 0.2 : 0.85};filter:${isPassed ? 'grayscale(1)' : 'none'};"><img src="${require('@/assets/map/direction.png')}" style="width:100%;height:auto;" /></div>`,
                 offset: new this.AMap.Pixel(-10, -5),
                 zIndex: 20,
                 bubble: true
-              });
-              overlays.push(directionMarker);
+              }));
             }
-            currentTargetDist += targetSpacing;
+            arrowDist += targetSpacing;
           }
-        }
 
-        // 为圆点保留原来的分布 logic (不受箭头影响)
-        const indices = this.pickEvenlySpacedIndices(totalPoints, 6);
+          // 路口圆点:已过置灰,未过正常
+          const indices = this.pickEvenlySpacedIndices(segmentPath.length, 6);
+          for (let i = 0; i < indices.length; i++) {
+            const idx = indices[i];
+            const p = segmentPath[idx];
+            const lng = Number(p[0]), lat = Number(p[1]);
+            if (Number.isNaN(lng) || Number.isNaN(lat)) continue;
+            const dotDist = dists[idx] || 0;
+            let markerType = 'normal';
+            if (i === 0) markerType = 'start';
+            else if (i === indices.length - 1) markerType = 'end';
+
+            const isPastDot = dotDist <= splitDist;
+            const dotConfig = isPastDot
+              ? { ...config, color: '#5A5A5A', id: `MOCK-D-${lineIdx}-${segmentIdx}-${idx}`, road: `勤务路线路口-${lineIdx}-${segmentIdx}-${idx}` }
+              : { ...config, id: `MOCK-D-${lineIdx}-${segmentIdx}-${idx}`, road: `勤务路线路口-${lineIdx}-${segmentIdx}-${idx}` };
+            const dotType = isPastDot && markerType === 'normal' ? 'passed' : markerType;
+            const marker = this.createTrafficLightMarker([lng, lat], dotConfig, dotType);
+            if (marker) overlays.push(marker);
+          }
+
+        } else {
+          // 非勤务路线 或 无 dutyState:原有逻辑
+          const polyline = new this.AMap.Polyline({
+            path: segmentPath,
+            strokeColor: line.color,
+            strokeWeight: 6,
+            strokeOpacity: 0.8,
+            zIndex: 15
+          });
+          overlays.push(polyline);
+
+          const totalPoints = segmentPath.length;
 
-        // 为第一个和最后一个圆点设置特殊类型
-        for (let i = 0; i < indices.length; i++) {
-          const idx = indices[i];
-          const p = segmentPath[idx];
-          const lng = Number(p[0]);
-          const lat = Number(p[1]);
-          if (Number.isNaN(lng) || Number.isNaN(lat)) continue;
+          const pathDistances = [0];
+          let totalPathDist = 0;
+          for (let j = 0; j < totalPoints - 1; j++) {
+            const d = this.calcApproxDistance(segmentPath[j], segmentPath[j+1]);
+            totalPathDist += d;
+            pathDistances.push(totalPathDist);
+          }
 
-          // 确定圆点类型:勤务路线保留起终点,干线协调移除起终点图标改为普通节点
-          let markerType = 'normal';
           if (configName !== '干线协调') {
-            if (i === 0) {
-              markerType = 'start';
-            } else if (i === indices.length - 1) {
-              markerType = 'end';
+            const targetSpacing = configName === '勤务路线' ? 0.0036 : 0.0018;
+            let currentTargetDist = targetSpacing / 2;
+
+            while (currentTargetDist < totalPathDist) {
+              let foundIdx = 0;
+              for (let j = 0; j < pathDistances.length - 1; j++) {
+                if (currentTargetDist >= pathDistances[j] && currentTargetDist <= pathDistances[j + 1]) {
+                  foundIdx = j;
+                  break;
+                }
+              }
+              const p1 = segmentPath[foundIdx];
+              const p2 = segmentPath[foundIdx + 1];
+              if (p1 && p2) {
+                const ratio = (currentTargetDist - pathDistances[foundIdx]) / (pathDistances[foundIdx + 1] - pathDistances[foundIdx]);
+                const midLng = Number(p1[0]) + (Number(p2[0]) - Number(p1[0])) * ratio;
+                const midLat = Number(p1[1]) + (Number(p2[1]) - Number(p1[1])) * ratio;
+                const bearing = this.calcBearingDeg(p1, p2);
+                const rotation = bearing - 90;
+                const directionMarker = new this.AMap.Marker({
+                  position: [midLng, midLat],
+                  content: `
+                    <div style="transform: rotate(${rotation}deg); width: 20px; height: 10px; display: flex; align-items: center; pointer-events: none; opacity: 0.85;">
+                      <img src="${require('@/assets/map/direction.png')}" style="width: 100%; height: auto;" />
+                    </div>
+                  `,
+                  offset: new this.AMap.Pixel(-10, -5),
+                  zIndex: 20,
+                  bubble: true
+                });
+                overlays.push(directionMarker);
+              }
+              currentTargetDist += targetSpacing;
             }
           }
 
-          const marker = this.createTrafficLightMarker([lng, lat], {
-            ...config,
-            id: `MOCK-${configName.charAt(0)}-${lineIdx}-${segmentIdx}-${idx}`,
-            road: `${configName}路口-${lineIdx}-${segmentIdx}-${idx}`
-          }, markerType);
-          if (marker) overlays.push(marker);
-        }
-      });
+          const indices = this.pickEvenlySpacedIndices(totalPoints, 6);
+          for (let i = 0; i < indices.length; i++) {
+            const idx = indices[i];
+            const p = segmentPath[idx];
+            const lng = Number(p[0]);
+            const lat = Number(p[1]);
+            if (Number.isNaN(lng) || Number.isNaN(lat)) continue;
+
+            let markerType = 'normal';
+            if (configName !== '干线协调') {
+              if (i === 0) markerType = 'start';
+              else if (i === indices.length - 1) markerType = 'end';
+            }
+
+            const marker = this.createTrafficLightMarker([lng, lat], {
+              ...config,
+              id: `MOCK-${configName.charAt(0)}-${lineIdx}-${segmentIdx}-${idx}`,
+              road: `${configName}路口-${lineIdx}-${segmentIdx}-${idx}`
+            }, markerType);
+            if (marker) overlays.push(marker);
+          }
+        } // end else
+
+      }); // end segments.forEach
 
       return overlays;
     },
@@ -749,6 +847,7 @@ export default {
         const isAbnormal = ["离线", "降级", "故障"].includes(config.name);
         const isRoute = ["干线协调", "勤务路线"].includes(config.name);
         const isStartEnd = type === 'start' || type === 'end';
+        const isPassed = type === 'passed';
 
         // 核心配置映射:减少嵌套逻辑
         const markerStyle = isStartEnd ? {
@@ -779,6 +878,12 @@ export default {
               <img src="${require(`@/assets/map/${type}.png`)}" style="width: 100%; height: auto; object-fit: contain; pointer-events: none;" />
             </div>
           `;
+        } 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>
+          `;
         } else if (isAbnormal) {
           const iconName = config.name === '离线' ? 'lixian' : config.name === '降级' ? 'jiangji' : 'guzhang';
           markerContent = `
@@ -1499,4 +1604,15 @@ export default {
 ::v-deep .pure-light-node:not(.abnormal-node) {
   border: 1px solid rgba(255, 255, 255, 0.4);
 }
+
+/* 勤务路线执行进度点脉冲动画 */
+::v-deep .duty-progress-node {
+  animation: duty-pulse 1.6s infinite ease-in-out;
+}
+
+@keyframes duty-pulse {
+  0%   { box-shadow: 0 0 0 3px rgba(188, 48, 29, 0.5); }
+  50%  { box-shadow: 0 0 0 9px rgba(188, 48, 29, 0.08); }
+  100% { box-shadow: 0 0 0 3px rgba(188, 48, 29, 0.5); }
+}
 </style>