|
|
@@ -2,7 +2,8 @@
|
|
|
<div class="map-wrapper">
|
|
|
<div ref="mapContainer" class="map-container"></div>
|
|
|
|
|
|
- <div class="map-legend" :style="privateStyle.legend" v-if="(!mode || mode === '路口')" :class="{ 'legend-hidden': !legendVisible }">
|
|
|
+ <div class="map-legend" :style="privateStyle.legend" v-if="(!mode || mode === '路口')"
|
|
|
+ :class="{ 'legend-hidden': !legendVisible }">
|
|
|
<div class="legend-header">
|
|
|
<div class="legend-title">图例</div>
|
|
|
<div class="legend-close-btn" @click="toggleLegend">✕</div>
|
|
|
@@ -16,7 +17,7 @@
|
|
|
|
|
|
<div v-for="item in legendConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
|
|
|
:class="{ 'is-inactive': !activeLegends.includes(item.name) }"
|
|
|
- v-if="!mode || (mode === '路口' && !['干线协调', '勤务路线'].includes(item.name))">
|
|
|
+ v-if="!mode || (mode === '路口' && !['干线协调', '勤务路线'].includes(item.name))">
|
|
|
|
|
|
<div class="legend-dot"
|
|
|
:style="{ backgroundColor: ['离线', '降级', '故障'].includes(item.name) ? 'transparent' : item.color }"
|
|
|
@@ -63,35 +64,35 @@ export default {
|
|
|
},
|
|
|
legendVisible: true,
|
|
|
activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
|
|
|
+ // 核心修正:增加生命周期标识,防止组件销毁后异步回调继续执行
|
|
|
+ _isDestroyed: false,
|
|
|
legendConfig: [
|
|
|
- // 横向主干道 (由北向南)
|
|
|
- { name: "中心计划", start: [116.6350, 39.9105], end: [116.6910, 39.9115], color: "#004CDE" }, // 新华大街全线
|
|
|
- { name: "干线协调", start: [116.6355, 39.9025], end: [116.6915, 39.9035], color: "#13C373" }, // 玉带河大街全线
|
|
|
- { name: "勤务路线", start: [116.6360, 39.8945], end: [116.6920, 39.8955], color: "#BC301D" }, // 运河西大街全线
|
|
|
- { name: "定周期控制", start: [116.6521, 39.9200], end: [116.6531, 39.8800], color: "#3296FA" }, // 车站路
|
|
|
- { name: "感应控制", start: [116.6611, 39.9205], end: [116.6615, 39.8805], color: "#FF864C" }, // 新华南路
|
|
|
- { name: "自适应控制", start: [116.6711, 39.9210], end: [116.6720, 39.8810], color: "#9F6EFE" }, // 东关大道
|
|
|
- { name: "手动控制", start: [116.6300, 39.9150], end: [116.6310, 39.8850], color: "#EB9F36" }, // 北苑南路
|
|
|
- { name: "特殊控制", start: [116.6820, 39.9215], end: [116.6825, 39.8815], color: "#A26218" }, // 临河里路
|
|
|
- { name: "离线", start: [116.6415, 39.9235], end: [116.6850, 39.9240], color: "#7A7A7A" }, // 北关大街-潞苑
|
|
|
- { name: "降级", start: [116.6365, 39.8850], end: [116.6800, 39.8860], color: "#D9C13B" }, // 万盛南街段
|
|
|
- { name: "故障", start: [116.6950, 39.9150], end: [116.6955, 39.885], color: "#FF3938" } // 潞通大街
|
|
|
+ { name: "中心计划", start: [116.6350, 39.9105], end: [116.6910, 39.9115], color: "#004CDE" },
|
|
|
+ { name: "干线协调", start: [116.6355, 39.9025], end: [116.6915, 39.9035], color: "#13C373" },
|
|
|
+ { name: "勤务路线", start: [116.6360, 39.8945], end: [116.6920, 39.8955], color: "#BC301D" },
|
|
|
+ { name: "定周期控制", start: [116.6521, 39.9200], end: [116.6531, 39.8800], color: "#3296FA" },
|
|
|
+ { name: "感应控制", start: [116.6611, 39.9205], end: [116.6615, 39.8805], color: "#FF864C" },
|
|
|
+ { name: "自适应控制", start: [116.6711, 39.9210], end: [116.6720, 39.8810], color: "#9F6EFE" },
|
|
|
+ { name: "手动控制", start: [116.6300, 39.9150], end: [116.6310, 39.8850], color: "#EB9F36" },
|
|
|
+ { name: "特殊控制", start: [116.6820, 39.9215], end: [116.6825, 39.8815], color: "#A26218" },
|
|
|
+ { name: "离线", start: [116.6415, 39.9235], end: [116.6850, 39.9240], color: "#7A7A7A" },
|
|
|
+ { name: "降级", start: [116.6365, 39.8850], end: [116.6800, 39.8860], color: "#D9C13B" },
|
|
|
+ { name: "故障", start: [116.6950, 39.9150], end: [116.6955, 39.885], color: "#FF3938" }
|
|
|
]
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
|
+ this._isDestroyed = false; // 重置标识
|
|
|
this.updateMapByMode();
|
|
|
-
|
|
|
this.initAMap();
|
|
|
|
|
|
- // 自定义首页地图搜索和图例位置样式
|
|
|
if (this.$route.path === '/home') {
|
|
|
this.privateStyle.legend = { right: "25%" };
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
mode: {
|
|
|
- handler(newMode) {
|
|
|
+ handler() {
|
|
|
this.updateMapByMode();
|
|
|
this.updateMapDisplay();
|
|
|
},
|
|
|
@@ -99,188 +100,163 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- if (this.infoWindow) this.infoWindow.close();
|
|
|
+ // 1. 立即设置销毁状态
|
|
|
+ this._isDestroyed = true;
|
|
|
|
|
|
- // 1. 清理普通的 polylines 数组
|
|
|
- this.polylines.forEach(p => {
|
|
|
- if (p && typeof p.setMap === 'function') p.setMap(null);
|
|
|
- });
|
|
|
+ // 2. 清理全局回调
|
|
|
+ window.closeMapInfoWindow = null;
|
|
|
|
|
|
- // 2. 核心修改:清理 routeGroups
|
|
|
- Object.values(this.routeGroups).forEach(g => {
|
|
|
- if (!g) return;
|
|
|
+ // 3. 关闭弹窗
|
|
|
+ if (this.infoWindow) {
|
|
|
+ this.infoWindow.close();
|
|
|
+ this.infoWindow = null;
|
|
|
+ }
|
|
|
|
|
|
- if (Array.isArray(g)) {
|
|
|
- // 如果是数组(当前的逻辑),遍历每个成员销毁
|
|
|
- g.forEach(overlay => {
|
|
|
- if (overlay && typeof overlay.setMap === 'function') {
|
|
|
- overlay.setMap(null);
|
|
|
- }
|
|
|
- });
|
|
|
- } else if (typeof g.setMap === 'function') {
|
|
|
- // 如果是旧版的 OverlayGroup 或单个覆盖物
|
|
|
- g.setMap(null);
|
|
|
+ // 4. 清理覆盖物引用
|
|
|
+ Object.values(this.routeGroups).forEach(overlays => {
|
|
|
+ if (Array.isArray(overlays)) {
|
|
|
+ overlays.forEach(o => o.setMap && o.setMap(null));
|
|
|
}
|
|
|
});
|
|
|
+ this.routeGroups = {};
|
|
|
|
|
|
+ // 5. 销毁地图实例并清空引用
|
|
|
if (this.map) {
|
|
|
this.map.destroy();
|
|
|
+ this.map = null;
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
- // 判断是否所有图例都在激活列表中
|
|
|
isAllSelected() {
|
|
|
return this.activeLegends.length === this.legendConfig.length;
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
+ // 检查地图环境是否安全可用
|
|
|
+ isMapSafe() {
|
|
|
+ return !this._isDestroyed && this.map && typeof this.map.add === 'function';
|
|
|
+ },
|
|
|
+
|
|
|
updateMapByMode() {
|
|
|
- // 根据mode调整activeLegends数组
|
|
|
switch (this.mode) {
|
|
|
case '路口':
|
|
|
- // 显示所有路线但去掉'干线协调'和'勤务路线'
|
|
|
this.activeLegends = ["中心计划", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"];
|
|
|
break;
|
|
|
case '干线':
|
|
|
- // 只显示'干线协调'路线
|
|
|
this.activeLegends = ["干线协调"];
|
|
|
break;
|
|
|
case '特勤':
|
|
|
- // 只显示'勤务路线'
|
|
|
this.activeLegends = ["勤务路线"];
|
|
|
break;
|
|
|
default:
|
|
|
- // 默认显示所有路线
|
|
|
- this.activeLegends = ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"];
|
|
|
+ this.activeLegends = this.legendConfig.map(item => item.name);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
updateMapDisplay() {
|
|
|
- // 关闭信息窗口
|
|
|
if (this.infoWindow) this.infoWindow.close();
|
|
|
-
|
|
|
- // 确保地图已初始化
|
|
|
- if (!this.map) return;
|
|
|
-
|
|
|
- // 根据activeLegends数组更新地图上路线和标记的显示/隐藏状态
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
+
|
|
|
Object.keys(this.routeGroups).forEach(name => {
|
|
|
const overlays = this.routeGroups[name];
|
|
|
if (overlays && overlays.length > 0) {
|
|
|
if (this.activeLegends.includes(name)) {
|
|
|
- // 如果在激活列表中,添加到地图
|
|
|
this.map.add(overlays);
|
|
|
} else {
|
|
|
- // 如果不在激活列表中,从地图中移除
|
|
|
this.map.remove(overlays);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
- // 修改后的 initAMap
|
|
|
+
|
|
|
async initAMap() {
|
|
|
window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
|
|
|
try {
|
|
|
- this.AMap = await AMapLoader.load({
|
|
|
+ const AMap = await AMapLoader.load({
|
|
|
key: this.amapKey,
|
|
|
version: "2.0",
|
|
|
- plugins: ['AMap.Driving', 'AMap.GeometryUtil']
|
|
|
+ plugins: ['AMap.Driving']
|
|
|
});
|
|
|
|
|
|
- this.map = new this.AMap.Map(this.$refs.mapContainer, {
|
|
|
+ // 异步回来后,首先检查组件是否还在
|
|
|
+ if (this._isDestroyed) return;
|
|
|
+
|
|
|
+ this.AMap = AMap;
|
|
|
+ this.map = new AMap.Map(this.$refs.mapContainer, {
|
|
|
zoom: 13.5,
|
|
|
mapStyle: "amap://styles/darkblue",
|
|
|
center: [116.663, 39.905],
|
|
|
});
|
|
|
|
|
|
- this.map.on('complete', () => this.drawStaticRoutes());
|
|
|
- } catch (err) { console.error('地图初始化失败', err); }
|
|
|
+ this.map.on('complete', () => {
|
|
|
+ if (!this._isDestroyed) this.drawStaticRoutes();
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ console.error('地图加载失败:', err);
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
drawStaticRoutes() {
|
|
|
- const AMap = this.AMap;
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
|
|
|
this.legendConfig.forEach((config, index) => {
|
|
|
- // 绘制所有路线,不根据mode过滤
|
|
|
- // 后续通过updateMapDisplay方法控制显示/隐藏
|
|
|
-
|
|
|
+ // 使用闭包防止异步回调拿到错误的 index 或 config
|
|
|
setTimeout(() => {
|
|
|
- const driving = new AMap.Driving({
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
+
|
|
|
+ const driving = new this.AMap.Driving({
|
|
|
map: null,
|
|
|
hideMarkers: true,
|
|
|
autoFitView: false
|
|
|
});
|
|
|
|
|
|
driving.search(config.start, config.end, (status, result) => {
|
|
|
- let markers = [];
|
|
|
- let path = [];
|
|
|
+ // 搜索结果返回时,组件可能已销毁
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
|
|
|
+ let path = [];
|
|
|
if (status === 'complete' && result.routes[0]) {
|
|
|
- const route = result.routes[0];
|
|
|
- route.steps.forEach(step => { path = path.concat(step.path); });
|
|
|
+ result.routes[0].steps.forEach(step => { path = path.concat(step.path); });
|
|
|
} else {
|
|
|
path = [config.start, config.end];
|
|
|
}
|
|
|
|
|
|
+ const markers = [];
|
|
|
let polyline = null;
|
|
|
- const needRouteLine = ["干线协调", "勤务路线"].includes(config.name);
|
|
|
|
|
|
- if (needRouteLine) {
|
|
|
- polyline = new AMap.Polyline({
|
|
|
+ if (["干线协调", "勤务路线"].includes(config.name)) {
|
|
|
+ polyline = new this.AMap.Polyline({
|
|
|
path: path,
|
|
|
strokeColor: config.color,
|
|
|
strokeWeight: 8,
|
|
|
strokeOpacity: 0.8,
|
|
|
- showDir: false,
|
|
|
- lineJoin: 'round',
|
|
|
- zIndex: 15,
|
|
|
- map: null
|
|
|
+ zIndex: 15
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- const points = [];
|
|
|
- const step = 10; // 步长越大,点越稀疏
|
|
|
+ // 抽稀取点逻辑
|
|
|
+ const step = 10;
|
|
|
for (let i = 0; i < path.length; i += step) {
|
|
|
- points.push(path[i]);
|
|
|
+ markers.push(this.createTrafficLightMarker(path[i], config));
|
|
|
}
|
|
|
-
|
|
|
- // 确保终点也被加上
|
|
|
if ((path.length - 1) % step !== 0) {
|
|
|
- points.push(path[path.length - 1]);
|
|
|
+ markers.push(this.createTrafficLightMarker(path[path.length - 1], config));
|
|
|
}
|
|
|
|
|
|
- points.forEach((pos, idx) => {
|
|
|
- // 临时逻辑,有真实接口后可以删除
|
|
|
- const posMap = {
|
|
|
- 8: 'pos1',
|
|
|
- 9: 'pos2',
|
|
|
- 10: 'pos3'
|
|
|
- };
|
|
|
-
|
|
|
- if (idx === 0 && posMap[index]) {
|
|
|
- localStorage.setItem(posMap[index], pos);
|
|
|
- }
|
|
|
- // 临时逻辑,有真实接口后可以删除
|
|
|
-
|
|
|
- markers.push(this.createTrafficLightMarker(pos, config));
|
|
|
- });
|
|
|
-
|
|
|
- // 3. 【关键修改】:overlays 数组只存放 markers,不再存放 polyline
|
|
|
const overlays = [...markers, polyline].filter(Boolean);
|
|
|
this.routeGroups[config.name] = overlays;
|
|
|
|
|
|
- // 确保地图已初始化
|
|
|
- if (this.map && this.activeLegends.includes(config.name)) {
|
|
|
+ // 最终添加到地图前再检查一次
|
|
|
+ if (this.isMapSafe() && this.activeLegends.includes(config.name)) {
|
|
|
this.map.add(overlays);
|
|
|
}
|
|
|
});
|
|
|
- }, index * 250);
|
|
|
+ }, index * 200);
|
|
|
});
|
|
|
},
|
|
|
|
|
|
createTrafficLightMarker(position, config) {
|
|
|
- // 根据业务需求:离线、降级、故障需要闪烁,其他保持静止
|
|
|
const needsFlash = ["离线", "降级", "故障"].includes(config.name);
|
|
|
- const displayStatus = needsFlash ? config.name : "正常运行";
|
|
|
const lng = Number(position[0] || position.lng);
|
|
|
const lat = Number(position[1] || position.lat);
|
|
|
|
|
|
@@ -297,8 +273,8 @@ export default {
|
|
|
extData: {
|
|
|
...config,
|
|
|
position: [lng, lat],
|
|
|
- statusColor: config.color, // 统一弹窗小圆点颜色
|
|
|
- statusLabel: displayStatus, // 统一弹窗状态文字
|
|
|
+ statusColor: config.color,
|
|
|
+ statusLabel: needsFlash ? config.name : "正常运行",
|
|
|
road: '北京路与南京路',
|
|
|
time: '2026.1.23.12:00'
|
|
|
}
|
|
|
@@ -306,18 +282,18 @@ export default {
|
|
|
|
|
|
marker.on('click', (e) => {
|
|
|
this.openLightInfo(e.target.getExtData(), e.lnglat);
|
|
|
- // 抛出地图路口点击事件
|
|
|
this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
return marker;
|
|
|
},
|
|
|
|
|
|
openLightInfo(data, position) {
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
+
|
|
|
const content = `
|
|
|
<div class="custom-info-card">
|
|
|
<div class="close-btn" onclick="window.closeMapInfoWindow()">✕</div>
|
|
|
-
|
|
|
<div class="card-header">
|
|
|
<div class="status-dot" style="background: ${data.statusColor}">
|
|
|
<span>${data.name.charAt(0)}</span>
|
|
|
@@ -325,19 +301,12 @@ export default {
|
|
|
<span class="status-text">${data.statusLabel}</span>
|
|
|
</div>
|
|
|
<div class="card-body">
|
|
|
- <div class="info-line">
|
|
|
- <span class="label">路口:</span>
|
|
|
- <span class="value">${data.road}</span>
|
|
|
- </div>
|
|
|
- <div class="info-line">
|
|
|
- <span class="label">发生时间:</span>
|
|
|
- <span class="value digital">${data.time}</span>
|
|
|
- </div>
|
|
|
+ <div class="info-line"><span class="label">路口:</span><span class="value">${data.road}</span></div>
|
|
|
+ <div class="info-line"><span class="label">发生时间:</span><span class="value digital">${data.time}</span></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
- // 定义全局关闭方法(因为 isCustom:true 下 Vue 事件会失效)
|
|
|
window.closeMapInfoWindow = () => {
|
|
|
if (this.infoWindow) this.infoWindow.close();
|
|
|
};
|
|
|
@@ -352,81 +321,58 @@ export default {
|
|
|
this.infoWindow.open(this.map, position);
|
|
|
},
|
|
|
|
|
|
- // 全选/全不选逻辑修正版
|
|
|
toggleAll() {
|
|
|
- const targetState = !this.isAllSelected; // 获取点击后的目标状态(true为全选,false为全不选)
|
|
|
+ const targetState = !this.isAllSelected;
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
|
|
|
if (targetState) {
|
|
|
- // --- 情况 1:执行“全选” ---
|
|
|
- // 1. 更新激活列表
|
|
|
this.activeLegends = this.legendConfig.map(item => item.name);
|
|
|
-
|
|
|
- // 2. 遍历所有已经生成的路线数组,将其全部添加到地图上
|
|
|
- Object.keys(this.routeGroups).forEach(name => {
|
|
|
- const overlays = this.routeGroups[name]; // 此时 routeGroups 存储的是数组
|
|
|
- if (overlays && overlays.length > 0) {
|
|
|
- this.map.add(overlays);
|
|
|
- }
|
|
|
+ Object.values(this.routeGroups).forEach(overlays => {
|
|
|
+ if (overlays && overlays.length > 0) this.map.add(overlays);
|
|
|
});
|
|
|
} else {
|
|
|
- // --- 情况 2:执行“全不选” ---
|
|
|
- // 1. 清空激活列表
|
|
|
this.activeLegends = [];
|
|
|
-
|
|
|
- // 2. 遍历所有路线数组,从地图上移除
|
|
|
- Object.keys(this.routeGroups).forEach(name => {
|
|
|
- const overlays = this.routeGroups[name];
|
|
|
- if (overlays && overlays.length > 0) {
|
|
|
- this.map.remove(overlays);
|
|
|
- }
|
|
|
+ Object.values(this.routeGroups).forEach(overlays => {
|
|
|
+ if (overlays && overlays.length > 0) this.map.remove(overlays);
|
|
|
});
|
|
|
-
|
|
|
- // 3. 关闭当前可能打开的弹窗
|
|
|
if (this.infoWindow) this.infoWindow.close();
|
|
|
}
|
|
|
},
|
|
|
|
|
|
toggleRouteVisible(name) {
|
|
|
- const overlays = this.routeGroups[name] || []; // 获取的是数组
|
|
|
+ if (!this.isMapSafe()) return;
|
|
|
+
|
|
|
+ const overlays = this.routeGroups[name] || [];
|
|
|
const index = this.activeLegends.indexOf(name);
|
|
|
if (index > -1) {
|
|
|
this.activeLegends.splice(index, 1);
|
|
|
- this.map.remove(overlays); // 改用 remove
|
|
|
+ this.map.remove(overlays);
|
|
|
if (this.infoWindow) this.infoWindow.close();
|
|
|
} else {
|
|
|
this.activeLegends.push(name);
|
|
|
- this.map.add(overlays); // 改用 add
|
|
|
+ this.map.add(overlays);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 其他组件点击定位到地图指定的点
|
|
|
focusByLocation(targetPos) {
|
|
|
- if (!targetPos || targetPos.length !== 2) return;
|
|
|
+ if (!this.isMapSafe() || !targetPos || targetPos.length !== 2) return;
|
|
|
|
|
|
let foundMarker = null;
|
|
|
-
|
|
|
- // 1. 遍历所有路线组
|
|
|
Object.values(this.routeGroups).forEach(group => {
|
|
|
- // 2. 在组内寻找 Marker
|
|
|
const marker = group.find(item => {
|
|
|
if (!(item instanceof this.AMap.Marker)) return false;
|
|
|
const pos = item.getExtData().position;
|
|
|
- // 3. 坐标比对(考虑到浮点数精度,建议使用 AMap 自带的几何工具或简单比对)
|
|
|
- return pos[0] === targetPos[0] && pos[1] === targetPos[1];
|
|
|
+ return Math.abs(pos[0] - targetPos[0]) < 0.0001 && Math.abs(pos[1] - targetPos[1]) < 0.0001;
|
|
|
});
|
|
|
if (marker) foundMarker = marker;
|
|
|
});
|
|
|
|
|
|
if (foundMarker) {
|
|
|
- // 4. 定位并打开弹窗
|
|
|
const finalPos = foundMarker.getPosition();
|
|
|
- this.map.setZoomAndCenter(17, finalPos, false, 500); // 17级视角,平滑移动
|
|
|
-
|
|
|
+ this.map.setZoomAndCenter(17, finalPos, false, 500);
|
|
|
setTimeout(() => {
|
|
|
- this.openLightInfo(foundMarker.getExtData(), finalPos);
|
|
|
+ if (!this._isDestroyed) this.openLightInfo(foundMarker.getExtData(), finalPos);
|
|
|
}, 600);
|
|
|
- } else {
|
|
|
- console.warn("未在地图上找到该坐标对应的点位:", targetPos);
|
|
|
}
|
|
|
},
|
|
|
|