|
|
@@ -318,7 +318,12 @@ export default {
|
|
|
{ start: [116.6445, 39.9075], end: [116.6846, 39.9075], color: "#13C373" }
|
|
|
],
|
|
|
"勤务路线": [
|
|
|
- { start: [116.7120, 39.9225], end: [116.7120, 39.8971], color: "#BC301D" }
|
|
|
+ // 第一段:未执行(全红)— 通州大街西段,干线协调下方约600m
|
|
|
+ { start: [116.6445, 39.8980], end: [116.6850, 39.8980], color: "#BC301D", dutyState: 'pending' },
|
|
|
+ // 第二段:执行中(进度45%)— 通州大街中段
|
|
|
+ { start: [116.6850, 39.8980], end: [116.7250, 39.8980], color: "#BC301D", dutyState: 'active', progress: 0.45 },
|
|
|
+ // 第三段:已执行完(全灰)— 通州大街东段
|
|
|
+ { start: [116.7250, 39.8980], end: [116.7650, 39.8980], color: "#BC301D", dutyState: 'done' }
|
|
|
]
|
|
|
};
|
|
|
|
|
|
@@ -501,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 = 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;
|
|
|
},
|
|
|
@@ -746,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 ? {
|
|
|
@@ -776,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 = `
|
|
|
@@ -1496,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>
|