|
|
@@ -13,11 +13,21 @@
|
|
|
|
|
|
<div v-for="item in legendConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
|
|
|
:class="{ 'is-inactive': !activeLegends.includes(item.name) }">
|
|
|
- <div class="legend-dot" :style="{ backgroundColor: item.color }">
|
|
|
- <span>{{ item.name.charAt(0) }}</span>
|
|
|
+
|
|
|
+ <div class="legend-dot"
|
|
|
+ :style="{ backgroundColor: ['离线', '降级', '故障'].includes(item.name) ? 'transparent' : item.color }"
|
|
|
+ :class="{ 'special-route': ['干线协调', '勤务路线'].includes(item.name), 'is-status-wrapper': ['离线', '降级', '故障'].includes(item.name) }">
|
|
|
+
|
|
|
+ <span v-if="!['离线', '降级', '故障'].includes(item.name)">
|
|
|
+ {{ item.name.charAt(0) }}
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <img v-else
|
|
|
+ :src="require(`@/assets/images/icon_${item.name === '离线' ? 'lixian' : item.name === '降级' ? 'jiangji' : 'guzhang'}.png`)"
|
|
|
+ class="status-icon" />
|
|
|
</div>
|
|
|
+
|
|
|
<div class="legend-label">{{ item.name }}</div>
|
|
|
- <!-- <div class="legend-status">{{ activeLegends.includes(item.name) ? '在线' : '离线' }}</div> -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -56,7 +66,7 @@ export default {
|
|
|
{ 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.8850], color: "#FF3938" } // 潞通大街
|
|
|
+ { name: "故障", start: [116.6950, 39.9150], end: [116.6955, 39.885], color: "#FF3938" } // 潞通大街
|
|
|
]
|
|
|
};
|
|
|
},
|
|
|
@@ -70,9 +80,32 @@ export default {
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
if (this.infoWindow) this.infoWindow.close();
|
|
|
- this.polylines.forEach(p => p.setMap(null));
|
|
|
- Object.values(this.routeGroups).forEach(g => g.setMap(null));
|
|
|
- if (this.map) this.map.destroy();
|
|
|
+
|
|
|
+ // 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);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (this.map) {
|
|
|
+ this.map.destroy();
|
|
|
+ }
|
|
|
},
|
|
|
computed: {
|
|
|
// 判断是否所有图例都在激活列表中
|
|
|
@@ -123,21 +156,6 @@ export default {
|
|
|
path = [config.start, config.end];
|
|
|
}
|
|
|
|
|
|
- // --- 【关键修改】:注释掉以下 Polyline 的定义 ---
|
|
|
- /*
|
|
|
- const polyline = new AMap.Polyline({
|
|
|
- path: path,
|
|
|
- strokeColor: config.color,
|
|
|
- strokeWeight: 8,
|
|
|
- strokeOpacity: 0.8,
|
|
|
- showDir: false,
|
|
|
- lineJoin: 'round',
|
|
|
- zIndex: 15,
|
|
|
- map: null
|
|
|
- });
|
|
|
- */
|
|
|
-
|
|
|
- // 1. 只有“干线协调”和“勤务路线”才创建路线对象
|
|
|
let polyline = null;
|
|
|
const needRouteLine = ["干线协调", "勤务路线"].includes(config.name);
|
|
|
|
|
|
@@ -154,24 +172,30 @@ export default {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-
|
|
|
- // 2. 在路径上分布点 (保持原样)
|
|
|
- // 修改 points 的采样逻辑,例如每隔 10 个坐标点取一个点
|
|
|
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]);
|
|
|
}
|
|
|
|
|
|
- // const points = path.length > 2
|
|
|
- // ? [path[0], path[Math.floor(path.length / 2)], path[path.length - 1]]
|
|
|
- // : [path[0], path[1]];
|
|
|
+ points.forEach((pos, idx) => {
|
|
|
+ // 临时逻辑,有真实接口后可以删除
|
|
|
+ const posMap = {
|
|
|
+ 8: 'pos1',
|
|
|
+ 9: 'pos2',
|
|
|
+ 10: 'pos3'
|
|
|
+ };
|
|
|
+
|
|
|
+ if (idx === 0 && posMap[index]) {
|
|
|
+ localStorage.setItem(posMap[index], pos);
|
|
|
+ }
|
|
|
+ // 临时逻辑,有真实接口后可以删除
|
|
|
|
|
|
- points.forEach(pos => {
|
|
|
markers.push(this.createTrafficLightMarker(pos, config));
|
|
|
});
|
|
|
|
|
|
@@ -179,12 +203,6 @@ export default {
|
|
|
const overlays = [...markers, polyline].filter(Boolean);
|
|
|
this.routeGroups[config.name] = overlays;
|
|
|
|
|
|
- // if (this.activeLegends.includes(config.name)) {
|
|
|
- // this.map.add(overlays);
|
|
|
- // // 这里的 setFitView 会根据点的位置自动聚焦
|
|
|
- // this.map.setFitView(overlays, false, [60, 60, 60, 60]);
|
|
|
- // }
|
|
|
-
|
|
|
if (this.activeLegends.includes(config.name)) {
|
|
|
this.map.add(overlays);
|
|
|
}
|
|
|
@@ -212,6 +230,7 @@ export default {
|
|
|
offset: new this.AMap.Pixel(-10, -10),
|
|
|
extData: {
|
|
|
...config,
|
|
|
+ position: [lng, lat],
|
|
|
statusColor: config.color, // 统一弹窗小圆点颜色
|
|
|
statusLabel: displayStatus, // 统一弹窗状态文字
|
|
|
road: '北京路与南京路',
|
|
|
@@ -223,35 +242,7 @@ export default {
|
|
|
return marker;
|
|
|
},
|
|
|
|
|
|
- // openLightInfo(data, position) {
|
|
|
- // const content = `
|
|
|
- // <div class="traffic-window">
|
|
|
- // <div class="window-header" style="background: ${data.lightDetail.color}">
|
|
|
- // <span class="title">路口信号机: ${data.name}</span>
|
|
|
- // </div>
|
|
|
- // <div class="window-body">
|
|
|
- // <div class="data-row"><span class="label">设备序列:</span><span>${data.lightDetail.sn}</span></div>
|
|
|
- // <div class="data-row">
|
|
|
- // <span class="label">当前相位:</span>
|
|
|
- // <span style="color: ${data.lightDetail.color}; font-weight:bold">${data.lightDetail.status}</span>
|
|
|
- // </div>
|
|
|
- // <div class="data-row"><span class="label">相位余时:</span><span class="highlight">${data.lightDetail.timeLeft}s</span></div>
|
|
|
- // <div class="data-row"><span class="label">运行模式:</span><span>智能感应</span></div>
|
|
|
- // <div class="progress-container">
|
|
|
- // <div class="progress-bar" style="width: ${(data.lightDetail.timeLeft / 60) * 100}%; background: ${data.lightDetail.color}"></div>
|
|
|
- // </div>
|
|
|
- // </div>
|
|
|
- // </div>
|
|
|
- // `;
|
|
|
-
|
|
|
- // 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);
|
|
|
- // },
|
|
|
openLightInfo(data, position) {
|
|
|
- // 在 openLightInfo 内部
|
|
|
const content = `
|
|
|
<div class="custom-info-card">
|
|
|
<div class="close-btn" onclick="window.closeMapInfoWindow()">✕</div>
|
|
|
@@ -290,20 +281,6 @@ export default {
|
|
|
this.infoWindow.open(this.map, position);
|
|
|
},
|
|
|
|
|
|
- // 全选/全不选逻辑
|
|
|
- // toggleAll() {
|
|
|
- // if (this.isAllSelected) {
|
|
|
- // // 如果当前是全选,则清空激活列表,并隐藏地图上所有组
|
|
|
- // this.activeLegends = [];
|
|
|
- // Object.values(this.routeGroups).forEach(group => group && group.hide());
|
|
|
- // if (this.infoWindow) this.infoWindow.close();
|
|
|
- // } else {
|
|
|
- // // 如果当前不是全选,则填充所有图例名称,并显示地图上所有组
|
|
|
- // this.activeLegends = this.legendConfig.map(item => item.name);
|
|
|
- // Object.values(this.routeGroups).forEach(group => group && group.show());
|
|
|
- // }
|
|
|
- // },
|
|
|
-
|
|
|
// 全选/全不选逻辑修正版
|
|
|
toggleAll() {
|
|
|
const targetState = !this.isAllSelected; // 获取点击后的目标状态(true为全选,false为全不选)
|
|
|
@@ -338,20 +315,6 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 保留你原有的单个切换方法,但确保逻辑一致
|
|
|
- // toggleRouteVisible(name) {
|
|
|
- // const group = this.routeGroups[name];
|
|
|
- // const index = this.activeLegends.indexOf(name);
|
|
|
- // if (index > -1) {
|
|
|
- // this.activeLegends.splice(index, 1);
|
|
|
- // group && group.hide();
|
|
|
- // this.infoWindow && this.infoWindow.close();
|
|
|
- // } else {
|
|
|
- // this.activeLegends.push(name);
|
|
|
- // group && group.show();
|
|
|
- // }
|
|
|
- // },
|
|
|
-
|
|
|
toggleRouteVisible(name) {
|
|
|
const overlays = this.routeGroups[name] || []; // 获取的是数组
|
|
|
const index = this.activeLegends.indexOf(name);
|
|
|
@@ -363,6 +326,37 @@ export default {
|
|
|
this.activeLegends.push(name);
|
|
|
this.map.add(overlays); // 改用 add
|
|
|
}
|
|
|
+ },
|
|
|
+
|
|
|
+ // 其他组件点击定位到地图指定的点
|
|
|
+ focusByLocation(targetPos) {
|
|
|
+ if (!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];
|
|
|
+ });
|
|
|
+ if (marker) foundMarker = marker;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (foundMarker) {
|
|
|
+ // 4. 定位并打开弹窗
|
|
|
+ const finalPos = foundMarker.getPosition();
|
|
|
+ this.map.setZoomAndCenter(17, finalPos, false, 500); // 17级视角,平滑移动
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ this.openLightInfo(foundMarker.getExtData(), finalPos);
|
|
|
+ }, 600);
|
|
|
+ } else {
|
|
|
+ console.warn("未在地图上找到该坐标对应的点位:", targetPos);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -381,7 +375,6 @@ export default {
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
-/* --- 红绿灯圆点:去掉数字后的纯净样式 --- */
|
|
|
::v-deep .pure-light-node {
|
|
|
width: 16px;
|
|
|
height: 16px;
|
|
|
@@ -396,9 +389,6 @@ export default {
|
|
|
}
|
|
|
|
|
|
::v-deep .pure-light-node span {
|
|
|
- display: inline-block;
|
|
|
- width: 16px;
|
|
|
- height: 16px;
|
|
|
display: flex;
|
|
|
transform: scale(0.75);
|
|
|
align-items: center;
|
|
|
@@ -423,7 +413,6 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* 关闭按钮样式 */
|
|
|
::v-deep .close-btn {
|
|
|
position: absolute;
|
|
|
top: 10px;
|
|
|
@@ -440,7 +429,6 @@ export default {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
-/* 确保容器相对定位,以便按钮定位 */
|
|
|
::v-deep .custom-info-card {
|
|
|
position: relative;
|
|
|
background: rgba(10, 15, 24, 0.95);
|
|
|
@@ -449,18 +437,6 @@ export default {
|
|
|
min-width: 200px;
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
|
-}
|
|
|
-
|
|
|
-/* --- 彻底还原图片的自定义弹窗 --- */
|
|
|
-::v-deep .custom-info-card {
|
|
|
- background: rgba(10, 15, 24, 0.95);
|
|
|
- /* 极深色背景 */
|
|
|
- border-radius: 10px;
|
|
|
- /* 较大的圆角 */
|
|
|
- padding: 12px 16px;
|
|
|
- min-width: 200px;
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
|
color: #fff;
|
|
|
}
|
|
|
|
|
|
@@ -478,10 +454,9 @@ export default {
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
margin-right: 8px;
|
|
|
- font-size: 11px;
|
|
|
- color: #000;
|
|
|
- /* 图标内文字为黑色 */
|
|
|
- font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 12px;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
::v-deep .status-dot span {
|
|
|
@@ -502,18 +477,15 @@ export default {
|
|
|
|
|
|
::v-deep .label {
|
|
|
color: #8da6c7;
|
|
|
- /* 标签灰色 */
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
::v-deep .value {
|
|
|
color: #ffffff;
|
|
|
- /* 内容白色 */
|
|
|
}
|
|
|
|
|
|
::v-deep .digital {
|
|
|
font-family: 'Consolas', monospace;
|
|
|
- /* 模拟数字字体 */
|
|
|
}
|
|
|
|
|
|
.map-legend {
|
|
|
@@ -549,10 +521,16 @@ export default {
|
|
|
}
|
|
|
|
|
|
.legend-dot {
|
|
|
+ margin-right: 10px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-dot:not(.is-status-wrapper) {
|
|
|
width: 20px;
|
|
|
height: 20px;
|
|
|
- margin-right: 10px;
|
|
|
- border-radius: 10px;
|
|
|
+ border-radius: 50%;
|
|
|
}
|
|
|
|
|
|
.legend-dot span {
|
|
|
@@ -566,6 +544,47 @@ export default {
|
|
|
line-height: 20px;
|
|
|
}
|
|
|
|
|
|
+.legend-dot.special-route {
|
|
|
+ position: relative;
|
|
|
+ overflow: visible !important;
|
|
|
+ z-index: 1;
|
|
|
+ border: none !important;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-dot.special-route span {
|
|
|
+ position: relative;
|
|
|
+ z-index: 3;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-dot.special-route::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ width: 150%;
|
|
|
+ height: 3px;
|
|
|
+ background-color: inherit;
|
|
|
+ opacity: 0.8;
|
|
|
+ transform: translate(-50%, -50%) rotate(-45deg);
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-dot.special-route::before {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 50%;
|
|
|
+ z-index: 2;
|
|
|
+ background-color: inherit;
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
.legend-label {
|
|
|
flex: 1;
|
|
|
color: #d0d9e2;
|
|
|
@@ -589,4 +608,21 @@ export default {
|
|
|
width: 10px;
|
|
|
height: 10px;
|
|
|
}
|
|
|
+
|
|
|
+.legend-dot.is-status-wrapper {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ background-color: transparent !important;
|
|
|
+ box-shadow: none !important;
|
|
|
+ border: none !important;
|
|
|
+ margin-right: 0;
|
|
|
+ transform: translateX(-15%);
|
|
|
+}
|
|
|
+
|
|
|
+.status-icon {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
</style>
|