|
|
@@ -2,8 +2,12 @@
|
|
|
<div class="map-wrapper">
|
|
|
<div ref="mapContainer" class="map-container"></div>
|
|
|
|
|
|
- <div class="map-legend" :style="privateStyle.legend">
|
|
|
- <div class="legend-title">图例</div>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
<div class="legend-list">
|
|
|
<div class="legend-item all-select" @click="toggleAll">
|
|
|
<div class="legend-dot"
|
|
|
@@ -12,7 +16,8 @@
|
|
|
</div>
|
|
|
|
|
|
<div v-for="item in legendConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
|
|
|
- :class="{ 'is-inactive': !activeLegends.includes(item.name) }">
|
|
|
+ :class="{ 'is-inactive': !activeLegends.includes(item.name) }"
|
|
|
+ v-if="!mode || (mode === '路口' && !['干线协调', '勤务路线'].includes(item.name))">
|
|
|
|
|
|
<div class="legend-dot"
|
|
|
:style="{ backgroundColor: ['离线', '降级', '故障'].includes(item.name) ? 'transparent' : item.color }"
|
|
|
@@ -31,6 +36,9 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="legend-show-btn" v-if="(!mode || mode === '路口') && !legendVisible" @click="toggleLegend">
|
|
|
+ <div class="legend-show-icon">☰</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -41,7 +49,8 @@ export default {
|
|
|
name: "TrafficMap",
|
|
|
props: {
|
|
|
amapKey: { type: String, default: '您的Key' },
|
|
|
- securityJsCode: { type: String, default: '您的安全密钥' }
|
|
|
+ securityJsCode: { type: String, default: '您的安全密钥' },
|
|
|
+ mode: { type: String, default: '', validator: (value) => ['', '路口', '干线', '特勤'].includes(value) }
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
@@ -53,205 +62,297 @@ export default {
|
|
|
privateStyle: {
|
|
|
legend: {}
|
|
|
},
|
|
|
+ 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() {
|
|
|
+ this.updateMapByMode();
|
|
|
+ this.updateMapDisplay();
|
|
|
+ },
|
|
|
+ immediate: false
|
|
|
+ }
|
|
|
+ },
|
|
|
beforeDestroy() {
|
|
|
- if (this.infoWindow) this.infoWindow.close();
|
|
|
-
|
|
|
- // 1. 清理普通的 polylines 数组
|
|
|
- this.polylines.forEach(p => {
|
|
|
- if (p && typeof p.setMap === 'function') p.setMap(null);
|
|
|
- });
|
|
|
-
|
|
|
- // 2. 核心修改:清理 routeGroups
|
|
|
- Object.values(this.routeGroups).forEach(g => {
|
|
|
- if (!g) return;
|
|
|
-
|
|
|
- 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);
|
|
|
+ // 1. 立即设置销毁状态
|
|
|
+ this._isDestroyed = true;
|
|
|
+
|
|
|
+ // 2. 关闭弹窗
|
|
|
+ if (this.infoWindow) {
|
|
|
+ try {
|
|
|
+ this.infoWindow.close();
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('关闭信息窗口时出错:', e);
|
|
|
}
|
|
|
- });
|
|
|
+ this.infoWindow = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 清理覆盖物引用
|
|
|
+ if (this.routeGroups) {
|
|
|
+ Object.values(this.routeGroups).forEach(overlays => {
|
|
|
+ if (Array.isArray(overlays)) {
|
|
|
+ overlays.forEach(o => {
|
|
|
+ try {
|
|
|
+ if (o.setMap) o.setMap(null);
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('清理覆盖物时出错:', e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.routeGroups = {};
|
|
|
+ }
|
|
|
|
|
|
+ // 4. 销毁地图实例并清空引用
|
|
|
if (this.map) {
|
|
|
- this.map.destroy();
|
|
|
+ try {
|
|
|
+ this.map.destroy();
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('销毁地图实例时出错:', e);
|
|
|
+ }
|
|
|
+ this.map = null;
|
|
|
}
|
|
|
+
|
|
|
+ // 5. 清理其他引用
|
|
|
+ this.AMap = null;
|
|
|
},
|
|
|
computed: {
|
|
|
- // 判断是否所有图例都在激活列表中
|
|
|
isAllSelected() {
|
|
|
return this.activeLegends.length === this.legendConfig.length;
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
- // 修改后的 initAMap
|
|
|
+ // 检查地图环境是否安全可用
|
|
|
+ isMapReady() {
|
|
|
+ return !this._isDestroyed && this.map && typeof this.map.add === 'function';
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMapByMode() {
|
|
|
+ switch (this.mode) {
|
|
|
+ case '路口':
|
|
|
+ this.activeLegends = ["中心计划", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"];
|
|
|
+ break;
|
|
|
+ case '干线':
|
|
|
+ this.activeLegends = ["干线协调"];
|
|
|
+ break;
|
|
|
+ case '特勤':
|
|
|
+ this.activeLegends = ["勤务路线"];
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ this.activeLegends = this.legendConfig.map(item => item.name);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMapDisplay() {
|
|
|
+ if (this.infoWindow) this.infoWindow.close();
|
|
|
+ if (!this.isMapReady()) 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
async initAMap() {
|
|
|
+ if (this._isDestroyed) return;
|
|
|
+
|
|
|
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.isMapReady()) return;
|
|
|
|
|
|
this.legendConfig.forEach((config, index) => {
|
|
|
+ // 使用箭头函数保持 this 指向 Vue 实例
|
|
|
setTimeout(() => {
|
|
|
- const driving = new AMap.Driving({
|
|
|
- map: null,
|
|
|
- hideMarkers: true,
|
|
|
- autoFitView: false
|
|
|
- });
|
|
|
-
|
|
|
- driving.search(config.start, config.end, (status, result) => {
|
|
|
- let markers = [];
|
|
|
- let path = [];
|
|
|
-
|
|
|
- if (status === 'complete' && result.routes[0]) {
|
|
|
- const route = result.routes[0];
|
|
|
- route.steps.forEach(step => { path = path.concat(step.path); });
|
|
|
- } else {
|
|
|
- path = [config.start, config.end];
|
|
|
- }
|
|
|
-
|
|
|
- let polyline = null;
|
|
|
- const needRouteLine = ["干线协调", "勤务路线"].includes(config.name);
|
|
|
-
|
|
|
- if (needRouteLine) {
|
|
|
- polyline = new AMap.Polyline({
|
|
|
- path: path,
|
|
|
- strokeColor: config.color,
|
|
|
- strokeWeight: 8,
|
|
|
- strokeOpacity: 0.8,
|
|
|
- showDir: false,
|
|
|
- lineJoin: 'round',
|
|
|
- zIndex: 15,
|
|
|
- map: null
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- const points = [];
|
|
|
- const step = 10; // 步长越大,点越稀疏
|
|
|
- for (let i = 0; i < path.length; i += step) {
|
|
|
- points.push(path[i]);
|
|
|
- }
|
|
|
-
|
|
|
- // 确保终点也被加上
|
|
|
- if ((path.length - 1) % step !== 0) {
|
|
|
- points.push(path[path.length - 1]);
|
|
|
- }
|
|
|
+ if (!this.isMapReady()) return;
|
|
|
|
|
|
- points.forEach((pos, idx) => {
|
|
|
- // 临时逻辑,有真实接口后可以删除
|
|
|
- const posMap = {
|
|
|
- 8: 'pos1',
|
|
|
- 9: 'pos2',
|
|
|
- 10: 'pos3'
|
|
|
- };
|
|
|
+ try {
|
|
|
+ const driving = new this.AMap.Driving({
|
|
|
+ map: null,
|
|
|
+ hideMarkers: true,
|
|
|
+ autoFitView: false
|
|
|
+ });
|
|
|
|
|
|
- if (idx === 0 && posMap[index]) {
|
|
|
- localStorage.setItem(posMap[index], pos);
|
|
|
+ // 这里必须用箭头函数 (status, result) => { ... }
|
|
|
+ driving.search(config.start, config.end, (status, result) => {
|
|
|
+ // 这里的 this 才能访问到 isMapReady
|
|
|
+ if (!this.isMapReady()) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ let path = [];
|
|
|
+ if (status === 'complete' && result && result.routes && result.routes[0]) {
|
|
|
+ result.routes[0].steps.forEach(step => { path = path.concat(step.path); });
|
|
|
+ } else {
|
|
|
+ path = [config.start, config.end];
|
|
|
+ }
|
|
|
+
|
|
|
+ const markers = [];
|
|
|
+ let polyline = null;
|
|
|
+
|
|
|
+ // 路线逻辑
|
|
|
+ if (["干线协调", "勤务路线"].includes(config.name)) {
|
|
|
+ polyline = new this.AMap.Polyline({
|
|
|
+ path: path,
|
|
|
+ strokeColor: config.color,
|
|
|
+ strokeWeight: 6,
|
|
|
+ strokeOpacity: 0.6,
|
|
|
+ zIndex: 15
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 稀疏化逻辑 ---
|
|
|
+ const isAbnormalStatus = ["离线", "降级", "故障"].includes(config.name);
|
|
|
+ // 异常状态(离线等)每 40 个点取一个,普通状态每 15 个点取一个
|
|
|
+ const stepSize = isAbnormalStatus ? 40 : 15;
|
|
|
+
|
|
|
+ for (let i = 0; i < path.length; i += stepSize) {
|
|
|
+ markers.push(this.createTrafficLightMarker(path[i], config));
|
|
|
+ }
|
|
|
+
|
|
|
+ const overlays = [...markers, polyline].filter(Boolean);
|
|
|
+ this.routeGroups[config.name] = overlays;
|
|
|
+
|
|
|
+ if (this.isMapReady() && this.activeLegends.includes(config.name)) {
|
|
|
+ this.map.add(overlays);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('处理路线数据时出错:', e);
|
|
|
}
|
|
|
- // 临时逻辑,有真实接口后可以删除
|
|
|
-
|
|
|
- markers.push(this.createTrafficLightMarker(pos, config));
|
|
|
});
|
|
|
-
|
|
|
- // 3. 【关键修改】:overlays 数组只存放 markers,不再存放 polyline
|
|
|
- const overlays = [...markers, polyline].filter(Boolean);
|
|
|
- this.routeGroups[config.name] = overlays;
|
|
|
-
|
|
|
- if (this.activeLegends.includes(config.name)) {
|
|
|
- this.map.add(overlays);
|
|
|
- }
|
|
|
- });
|
|
|
- }, index * 250);
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('创建驾车实例时出错:', e);
|
|
|
+ }
|
|
|
+ }, 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);
|
|
|
-
|
|
|
- const marker = new this.AMap.Marker({
|
|
|
- position: [lng, lat],
|
|
|
- zIndex: 100,
|
|
|
- content: `
|
|
|
- <div class="pure-light-node ${needsFlash ? 'breathe' : ''}"
|
|
|
- style="background: ${config.color}; box-shadow: 0 0 15px ${config.color}; font-size: 12px; display: flex; justify-content: center; align-items: center; color: #fff; padding: 8px;">
|
|
|
- <span>${config.name.charAt(0)}</span>
|
|
|
+ if (!position || !config) return null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const isAbnormal = ["离线", "降级", "故障"].includes(config.name);
|
|
|
+ const lng = Number(position[0] || position.lng);
|
|
|
+ const lat = Number(position[1] || position.lat);
|
|
|
+
|
|
|
+ // 验证坐标有效性
|
|
|
+ if (isNaN(lng) || isNaN(lat)) return null;
|
|
|
+
|
|
|
+ // 3. 【视觉优化】调整 Marker 大小比例
|
|
|
+ // 异常状态图标略大(为了警示),普通点位略小且半透明
|
|
|
+ const size = isAbnormal ? '18px' : '14px';
|
|
|
+ const opacity = isAbnormal ? '1' : '0.85';
|
|
|
+ const shadow = isAbnormal ? `0 0 10px ${config.color || '#999'}` : `0 0 5px ${config.color || '#999'}`;
|
|
|
+
|
|
|
+ const marker = new this.AMap.Marker({
|
|
|
+ position: [lng, lat],
|
|
|
+ zIndex: isAbnormal ? 110 : 100, // 异常图标显示在更上层
|
|
|
+ content: `
|
|
|
+ <div class="pure-light-node ${isAbnormal ? 'breathe abnormal-node' : ''}"
|
|
|
+ style="
|
|
|
+ width: ${size};
|
|
|
+ height: ${size};
|
|
|
+ background: ${config.color || '#999'};
|
|
|
+ box-shadow: ${shadow};
|
|
|
+ opacity: ${opacity};
|
|
|
+ border: 1.5px solid rgba(255,255,255,0.7);
|
|
|
+ font-size: 12px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ color: #fff;
|
|
|
+ padding: 8px;
|
|
|
+ ">
|
|
|
+ <span style="transform: scale(0.7); font-weight: bold;">${config.name.charAt(0)}</span>
|
|
|
</div>
|
|
|
`,
|
|
|
- offset: new this.AMap.Pixel(-10, -10),
|
|
|
+ offset: new this.AMap.Pixel(-8, -8),
|
|
|
extData: {
|
|
|
...config,
|
|
|
position: [lng, lat],
|
|
|
- statusColor: config.color, // 统一弹窗小圆点颜色
|
|
|
- statusLabel: displayStatus, // 统一弹窗状态文字
|
|
|
+ statusColor: config.color || '#999',
|
|
|
+ statusLabel: isAbnormal ? config.name : "正常运行",
|
|
|
road: '北京路与南京路',
|
|
|
time: '2026.1.23.12:00'
|
|
|
}
|
|
|
});
|
|
|
|
|
|
marker.on('click', (e) => {
|
|
|
- this.openLightInfo(e.target.getExtData(), e.lnglat);
|
|
|
- // 抛出地图路口点击事件
|
|
|
- this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
|
|
|
+ if (!this._isDestroyed) {
|
|
|
+ this.openLightInfo(e.target.getExtData(), e.lnglat);
|
|
|
+ this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
|
|
|
+ }
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
return marker;
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('创建标记时出错:', e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
openLightInfo(data, position) {
|
|
|
+ if (!this.isMapReady()) return;
|
|
|
+
|
|
|
+ const infoWindowId = `info-window-${Date.now()}`;
|
|
|
const content = `
|
|
|
- <div class="custom-info-card">
|
|
|
- <div class="close-btn" onclick="window.closeMapInfoWindow()">✕</div>
|
|
|
-
|
|
|
+ <div class="custom-info-card" id="${infoWindowId}">
|
|
|
+ <div class="close-btn" data-id="${infoWindowId}">✕</div>
|
|
|
<div class="card-header">
|
|
|
<div class="status-dot" style="background: ${data.statusColor}">
|
|
|
<span>${data.name.charAt(0)}</span>
|
|
|
@@ -259,109 +360,90 @@ 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();
|
|
|
- };
|
|
|
-
|
|
|
if (!this.infoWindow) {
|
|
|
this.infoWindow = new this.AMap.InfoWindow({
|
|
|
isCustom: true,
|
|
|
offset: new this.AMap.Pixel(0, -20)
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
this.infoWindow.setContent(content);
|
|
|
this.infoWindow.open(this.map, position);
|
|
|
+
|
|
|
+ // 添加关闭按钮事件监听器
|
|
|
+ setTimeout(() => {
|
|
|
+ const closeBtn = document.querySelector(`#${infoWindowId} .close-btn`);
|
|
|
+ if (closeBtn) {
|
|
|
+ closeBtn.addEventListener('click', () => {
|
|
|
+ if (this.infoWindow) this.infoWindow.close();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
},
|
|
|
|
|
|
- // 全选/全不选逻辑修正版
|
|
|
toggleAll() {
|
|
|
- const targetState = !this.isAllSelected; // 获取点击后的目标状态(true为全选,false为全不选)
|
|
|
+ const targetState = !this.isAllSelected;
|
|
|
+ if (!this.isMapReady()) 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.isMapReady()) 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.isMapReady() || !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);
|
|
|
}
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleLegend() {
|
|
|
+ this.legendVisible = !this.legendVisible;
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -380,14 +462,7 @@ export default {
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
-::v-deep .pure-light-node {
|
|
|
- width: 16px;
|
|
|
- height: 16px;
|
|
|
- border-radius: 50%;
|
|
|
- border: 2px solid rgba(255, 255, 255, 0.8);
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s;
|
|
|
-}
|
|
|
+
|
|
|
|
|
|
::v-deep .pure-light-node.breathe {
|
|
|
animation: light-breathe 2s infinite ease-in-out;
|
|
|
@@ -404,19 +479,7 @@ export default {
|
|
|
filter: brightness(1.2);
|
|
|
}
|
|
|
|
|
|
-@keyframes light-breathe {
|
|
|
-
|
|
|
- 0%,
|
|
|
- 100% {
|
|
|
- opacity: 0.7;
|
|
|
- transform: scale(1);
|
|
|
- }
|
|
|
|
|
|
- 50% {
|
|
|
- opacity: 1;
|
|
|
- transform: scale(1.15);
|
|
|
- }
|
|
|
-}
|
|
|
|
|
|
::v-deep .close-btn {
|
|
|
position: absolute;
|
|
|
@@ -502,16 +565,76 @@ export default {
|
|
|
padding: 15px;
|
|
|
border-radius: 6px;
|
|
|
z-index: 100;
|
|
|
+ transition: all 0.3s ease-in-out;
|
|
|
+ opacity: 1;
|
|
|
+ max-width: 200px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-.legend-title {
|
|
|
- color: #fff;
|
|
|
- font-size: 14px;
|
|
|
+.map-legend.legend-hidden {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateX(calc(100% + 20px));
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
margin-bottom: 12px;
|
|
|
border-bottom: 1px solid #1e4d8e;
|
|
|
padding-bottom: 8px;
|
|
|
}
|
|
|
|
|
|
+.legend-close-btn {
|
|
|
+ color: #8da6c7;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 16px;
|
|
|
+ transition: color 0.3s;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-close-btn:hover {
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-show-btn {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 30px;
|
|
|
+ right: 20px;
|
|
|
+ background: rgba(5, 22, 45, 0.9);
|
|
|
+ border: 1px solid #1e4d8e;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 6px 0 0 6px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+ z-index: 99;
|
|
|
+ transition: all 0.3s ease-in-out;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-show-btn:hover {
|
|
|
+ background: rgba(10, 30, 60, 0.9);
|
|
|
+ border-color: #3a75c4;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-show-icon {
|
|
|
+ color: #8da6c7;
|
|
|
+ font-size: 18px;
|
|
|
+ transition: color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-show-btn:hover .legend-show-icon {
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-title {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
.legend-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -630,4 +753,42 @@ export default {
|
|
|
object-fit: contain;
|
|
|
display: block;
|
|
|
}
|
|
|
+
|
|
|
+::v-deep .pure-light-node {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2px solid rgba(255, 255, 255, 0.8);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ color: #fff;
|
|
|
+ pointer-events: auto;
|
|
|
+ /* 确保能点击 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 异常状态增加稍微剧烈一点的呼吸感,但缩小范围 */
|
|
|
+@keyframes light-breathe {
|
|
|
+ 0% {
|
|
|
+ transform: scale(0.9);
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+
|
|
|
+ 50% {
|
|
|
+ transform: scale(1.1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ transform: scale(0.9);
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 首页展示时,如果觉得还是太密,可以给非异常节点降权 */
|
|
|
+::v-deep .pure-light-node:not(.abnormal-node) {
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
</style>
|