|
|
@@ -15,9 +15,8 @@
|
|
|
<div class="legend-label" style="font-weight: bold;">全选</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-for="item in statusConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
|
|
|
- :class="{ 'is-inactive': !activeLegends.includes(item.name) }"
|
|
|
- v-if="!mode || (mode === '路口' && !['干线协调', '勤务路线'].includes(item.name))">
|
|
|
+ <div v-for="item in legendStatusConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
|
|
|
+ :class="{ 'is-inactive': !activeLegends.includes(item.name) }">
|
|
|
|
|
|
<div class="legend-dot"
|
|
|
:style="{ backgroundColor: ['离线', '降级', '故障'].includes(item.name) ? 'transparent' : item.color }"
|
|
|
@@ -68,7 +67,9 @@ export default {
|
|
|
legendVisible: true,
|
|
|
activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
|
|
|
// 核心修正:增加生命周期标识,防止组件销毁后异步回调继续执行
|
|
|
- _isDestroyed: false,
|
|
|
+ isComponentDestroyed: false,
|
|
|
+ drawSeq: 0,
|
|
|
+ driving: null,
|
|
|
// 状态类型配置
|
|
|
statusConfig: [
|
|
|
{ name: "中心计划", color: "#004CDE", type: "normal" },
|
|
|
@@ -91,7 +92,7 @@ export default {
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
|
- this._isDestroyed = false; // 重置标识
|
|
|
+ this.isComponentDestroyed = false; // 重置标识
|
|
|
this.loadMapData().then(() => {
|
|
|
this.classifyIntersectionsByStatus();
|
|
|
this.updateMapByMode();
|
|
|
@@ -113,7 +114,8 @@ export default {
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
// 1. 立即设置销毁状态
|
|
|
- this._isDestroyed = true;
|
|
|
+ this.isComponentDestroyed = true;
|
|
|
+ this.drawSeq += 1;
|
|
|
|
|
|
// 2. 关闭弹窗
|
|
|
if (this.infoWindow) {
|
|
|
@@ -153,10 +155,18 @@ export default {
|
|
|
|
|
|
// 5. 清理其他引用
|
|
|
this.AMap = null;
|
|
|
+ this.driving = null;
|
|
|
},
|
|
|
computed: {
|
|
|
isAllSelected() {
|
|
|
return this.activeLegends.length === this.statusConfig.length;
|
|
|
+ },
|
|
|
+ legendStatusConfig() {
|
|
|
+ if (!this.mode) return this.statusConfig;
|
|
|
+ if (this.mode === '路口') {
|
|
|
+ return this.statusConfig.filter(item => !['干线协调', '勤务路线'].includes(item.name));
|
|
|
+ }
|
|
|
+ return [];
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -174,7 +184,7 @@ export default {
|
|
|
|
|
|
// 检查地图环境是否安全可用
|
|
|
isMapReady() {
|
|
|
- return !this._isDestroyed && this.map && typeof this.map.add === 'function';
|
|
|
+ return !this.isComponentDestroyed && this.map && typeof this.map.add === 'function';
|
|
|
},
|
|
|
|
|
|
// 将真实路口数据按状态类型分类
|
|
|
@@ -233,7 +243,7 @@ export default {
|
|
|
},
|
|
|
|
|
|
async initAMap() {
|
|
|
- if (this._isDestroyed) return;
|
|
|
+ if (this.isComponentDestroyed) return;
|
|
|
|
|
|
// 确保在加载前注入
|
|
|
window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
|
|
|
@@ -246,20 +256,22 @@ export default {
|
|
|
plugins: ['AMap.Driving']
|
|
|
});
|
|
|
|
|
|
- if (this._isDestroyed) return;
|
|
|
+ if (this.isComponentDestroyed) return;
|
|
|
|
|
|
this.AMap = AMap;
|
|
|
this.map = new AMap.Map(this.$refs.mapContainer, {
|
|
|
- zoom: 14.2,
|
|
|
+ zoom: 15,
|
|
|
mapStyle: "amap://styles/darkblue",
|
|
|
center: [116.663, 39.905], // 通州区中心
|
|
|
});
|
|
|
|
|
|
+ this.driving = new AMap.Driving({ map: null, hideMarkers: true });
|
|
|
+
|
|
|
|
|
|
|
|
|
// 建议在地图加载完成后再画线
|
|
|
this.map.on('complete', () => {
|
|
|
- if (!this._isDestroyed) {
|
|
|
+ if (!this.isComponentDestroyed) {
|
|
|
this.drawStaticRoutes();
|
|
|
// 临时方案,实际项目中删除 按4:3:3比例提取故障、离线、降级坐标点并存储到localStorage
|
|
|
this.storeStatusCoordsToLocalStorage();
|
|
|
@@ -273,9 +285,14 @@ export default {
|
|
|
// 绘制静态路线和标记
|
|
|
// 对于普通状态,从 mock 数据加载路口标记
|
|
|
// 对于路线类(干线协调和勤务路线),使用高德地图路线规划绘制路线和密集点位
|
|
|
- drawStaticRoutes() {
|
|
|
+ async drawStaticRoutes() {
|
|
|
if (!this.isMapReady()) return;
|
|
|
|
|
|
+ this.drawSeq += 1;
|
|
|
+ const drawSeq = this.drawSeq;
|
|
|
+
|
|
|
+ this.clearAllRouteOverlays();
|
|
|
+
|
|
|
const realRouteConfigs = {
|
|
|
"干线协调": [
|
|
|
{ start: [116.6421, 39.9172], end: [116.6825, 39.9172], color: "#13C373" },
|
|
|
@@ -295,7 +312,9 @@ export default {
|
|
|
]
|
|
|
};
|
|
|
|
|
|
- this.statusConfig.forEach((config) => {
|
|
|
+ for (const config of this.statusConfig) {
|
|
|
+ if (this.isComponentDestroyed || drawSeq !== this.drawSeq) return;
|
|
|
+
|
|
|
// 1. 处理普通非路线状态(从 mock 数据加载)
|
|
|
if (!realRouteConfigs[config.name]) {
|
|
|
const intersections = this.statusIntersections[config.name] || [];
|
|
|
@@ -304,59 +323,181 @@ export default {
|
|
|
).filter(Boolean);
|
|
|
this.routeGroups[config.name] = markers;
|
|
|
if (this.activeLegends.includes(config.name)) this.map.add(markers);
|
|
|
- return;
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
// 2. 处理路线类(干线/特勤)
|
|
|
- if (!this.routeGroups[config.name]) this.routeGroups[config.name] = [];
|
|
|
- const driving = new this.AMap.Driving({ map: null, hideMarkers: true });
|
|
|
-
|
|
|
- realRouteConfigs[config.name].forEach((line, lineIdx) => {
|
|
|
- driving.search(line.start, line.end, (status, result) => {
|
|
|
- if (status === 'complete' && result.routes[0]) {
|
|
|
- const route = result.routes[0];
|
|
|
- const fullPath = [];
|
|
|
-
|
|
|
- // 提取完整路径用于画线
|
|
|
- route.steps.forEach(step => fullPath.push(...step.path));
|
|
|
-
|
|
|
- const segments = this.extractMainStraightSegments(fullPath);
|
|
|
-
|
|
|
- segments.forEach((segmentPath, segmentIdx) => {
|
|
|
- if (segmentPath.length < 2) return;
|
|
|
-
|
|
|
- const polyline = new this.AMap.Polyline({
|
|
|
- path: segmentPath,
|
|
|
- strokeColor: line.color,
|
|
|
- strokeWeight: 6,
|
|
|
- strokeOpacity: 0.8,
|
|
|
- zIndex: 15
|
|
|
- });
|
|
|
- this.routeGroups[config.name].push(polyline);
|
|
|
-
|
|
|
- const totalPoints = segmentPath.length;
|
|
|
- const stepSize = Math.max(Math.floor(totalPoints / 10), 1);
|
|
|
-
|
|
|
- for (let i = 0; i < totalPoints; i += stepSize) {
|
|
|
- const p = segmentPath[i];
|
|
|
- const marker = this.createTrafficLightMarker([p.lng, p.lat], {
|
|
|
- ...config,
|
|
|
- id: `MOCK-${config.name.charAt(0)}-${lineIdx}-${segmentIdx}-${i}`,
|
|
|
- road: `${config.name}路口-${lineIdx}-${segmentIdx}-${i}`
|
|
|
- });
|
|
|
- if (marker) this.routeGroups[config.name].push(marker);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- if (this.activeLegends.includes(config.name)) {
|
|
|
- this.map.add(this.routeGroups[config.name]);
|
|
|
- }
|
|
|
- }
|
|
|
+ this.routeGroups[config.name] = [];
|
|
|
+ const lines = realRouteConfigs[config.name] || [];
|
|
|
+
|
|
|
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx += 1) {
|
|
|
+ if (this.isComponentDestroyed || drawSeq !== this.drawSeq) return;
|
|
|
+
|
|
|
+ const line = lines[lineIdx];
|
|
|
+ let path = null;
|
|
|
+ try {
|
|
|
+ path = await this.searchDrivingPathWithRetry(line.start, line.end);
|
|
|
+ } catch (e) {
|
|
|
+ path = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const overlays = this.buildRouteOverlaysFromPath({
|
|
|
+ config,
|
|
|
+ configName: config.name,
|
|
|
+ line,
|
|
|
+ lineIdx,
|
|
|
+ path
|
|
|
});
|
|
|
+
|
|
|
+ if (this.isComponentDestroyed || drawSeq !== this.drawSeq) return;
|
|
|
+
|
|
|
+ if (overlays.length > 0) {
|
|
|
+ this.routeGroups[config.name].push(...overlays);
|
|
|
+ if (this.activeLegends.includes(config.name)) this.map.add(overlays);
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.sleep(80);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ clearAllRouteOverlays() {
|
|
|
+ if (!this.isMapReady()) return;
|
|
|
+ try {
|
|
|
+ if (this.infoWindow) this.infoWindow.close();
|
|
|
+ } catch (e) {
|
|
|
+ void e;
|
|
|
+ }
|
|
|
+
|
|
|
+ Object.values(this.routeGroups || {}).forEach(overlays => {
|
|
|
+ if (!Array.isArray(overlays) || overlays.length === 0) return;
|
|
|
+ try {
|
|
|
+ this.map.remove(overlays);
|
|
|
+ } catch (e) {
|
|
|
+ void e;
|
|
|
+ }
|
|
|
+ overlays.forEach(o => {
|
|
|
+ try {
|
|
|
+ if (o && typeof o.setMap === 'function') o.setMap(null);
|
|
|
+ } catch (e) {
|
|
|
+ void e;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ this.routeGroups = {};
|
|
|
+ },
|
|
|
+
|
|
|
+ sleep(ms) {
|
|
|
+ return new Promise(resolve => setTimeout(resolve, ms));
|
|
|
+ },
|
|
|
+
|
|
|
+ async searchDrivingPathWithRetry(start, end) {
|
|
|
+ if (!this.AMap || !this.driving || typeof this.driving.search !== 'function') {
|
|
|
+ throw new Error('Driving not ready');
|
|
|
+ }
|
|
|
+
|
|
|
+ const maxAttempts = 3;
|
|
|
+ let lastErr = null;
|
|
|
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
|
+ try {
|
|
|
+ return await this.withTimeout(this.searchDrivingPathOnce(start, end), 8000);
|
|
|
+ } catch (e) {
|
|
|
+ lastErr = e;
|
|
|
+ await this.sleep(250 * attempt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw lastErr || new Error('Driving search failed');
|
|
|
+ },
|
|
|
+
|
|
|
+ searchDrivingPathOnce(start, end) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ this.driving.search(start, end, (status, result) => {
|
|
|
+ const route = result && result.routes && result.routes[0];
|
|
|
+ if (status === 'complete' && route && Array.isArray(route.steps)) {
|
|
|
+ const fullPath = [];
|
|
|
+ route.steps.forEach(step => {
|
|
|
+ if (step && Array.isArray(step.path)) fullPath.push(...step.path);
|
|
|
+ });
|
|
|
+ if (fullPath.length >= 2) resolve(fullPath);
|
|
|
+ else reject(new Error('empty_path'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ reject(new Error(typeof status === 'string' ? status : 'driving_error'));
|
|
|
});
|
|
|
});
|
|
|
},
|
|
|
|
|
|
+ withTimeout(promise, timeoutMs) {
|
|
|
+ return Promise.race([
|
|
|
+ promise,
|
|
|
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs))
|
|
|
+ ]);
|
|
|
+ },
|
|
|
+
|
|
|
+ buildRouteOverlaysFromPath({ config, configName, line, lineIdx, path }) {
|
|
|
+ if (!this.AMap || !this.map) return [];
|
|
|
+
|
|
|
+ let basePath = path;
|
|
|
+ if (!Array.isArray(basePath) || basePath.length < 2) {
|
|
|
+ basePath = this.buildFallbackLinePath(line.start, line.end, 30);
|
|
|
+ }
|
|
|
+
|
|
|
+ const segments = this.extractMainStraightSegments(basePath);
|
|
|
+ const overlays = [];
|
|
|
+
|
|
|
+ segments.forEach((segmentPath, segmentIdx) => {
|
|
|
+ if (!Array.isArray(segmentPath) || segmentPath.length < 2) return;
|
|
|
+
|
|
|
+ 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;
|
|
|
+ const stepSize = Math.max(Math.floor(totalPoints / 10), 1);
|
|
|
+
|
|
|
+ for (let i = 0; i < totalPoints; i += stepSize) {
|
|
|
+ const p = segmentPath[i];
|
|
|
+ const lng = p && typeof p.lng === 'number' ? p.lng : (Array.isArray(p) ? Number(p[0]) : NaN);
|
|
|
+ const lat = p && typeof p.lat === 'number' ? p.lat : (Array.isArray(p) ? Number(p[1]) : NaN);
|
|
|
+ if (Number.isNaN(lng) || Number.isNaN(lat)) continue;
|
|
|
+
|
|
|
+ const marker = this.createTrafficLightMarker([lng, lat], {
|
|
|
+ ...config,
|
|
|
+ id: `MOCK-${configName.charAt(0)}-${lineIdx}-${segmentIdx}-${i}`,
|
|
|
+ road: `${configName}路口-${lineIdx}-${segmentIdx}-${i}`
|
|
|
+ });
|
|
|
+ if (marker) overlays.push(marker);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return overlays;
|
|
|
+ },
|
|
|
+
|
|
|
+ buildFallbackLinePath(start, end, pointCount) {
|
|
|
+ const sLng = Number(start && start[0]);
|
|
|
+ const sLat = Number(start && start[1]);
|
|
|
+ const eLng = Number(end && end[0]);
|
|
|
+ const eLat = Number(end && end[1]);
|
|
|
+ const n = Math.max(Number(pointCount) || 2, 2);
|
|
|
+ const path = [];
|
|
|
+
|
|
|
+ if ([sLng, sLat, eLng, eLat].some(v => Number.isNaN(v))) return path;
|
|
|
+
|
|
|
+ for (let i = 0; i < n; i += 1) {
|
|
|
+ const t = n === 1 ? 0 : i / (n - 1);
|
|
|
+ const lng = sLng + (eLng - sLng) * t;
|
|
|
+ const lat = sLat + (eLat - sLat) * t;
|
|
|
+ path.push({ lng, lat });
|
|
|
+ }
|
|
|
+ return path;
|
|
|
+ },
|
|
|
+
|
|
|
extractMainStraightSegments(path) {
|
|
|
const points = (path || []).filter(p => p && typeof p.lng === 'number' && typeof p.lat === 'number');
|
|
|
if (points.length < 2) return [];
|
|
|
@@ -523,7 +664,7 @@ export default {
|
|
|
});
|
|
|
|
|
|
marker.on('click', (e) => {
|
|
|
- if (!this._isDestroyed) {
|
|
|
+ if (!this.isComponentDestroyed) {
|
|
|
this.openLightInfo(e.target.getExtData(), e.lnglat);
|
|
|
this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
|
|
|
}
|
|
|
@@ -638,7 +779,7 @@ export default {
|
|
|
const finalPos = foundMarker.getPosition();
|
|
|
this.map.setZoomAndCenter(17, finalPos, false, 500);
|
|
|
setTimeout(() => {
|
|
|
- if (!this._isDestroyed) this.openLightInfo(foundMarker.getExtData(), finalPos);
|
|
|
+ if (!this.isComponentDestroyed) this.openLightInfo(foundMarker.getExtData(), finalPos);
|
|
|
}, 600);
|
|
|
}
|
|
|
},
|