|
|
@@ -56,23 +56,19 @@ export default {
|
|
|
AMap: null,
|
|
|
map: null,
|
|
|
infoWindow: null,
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
routeGroups: {},
|
|
|
+ dotMarkers: [],
|
|
|
privateStyle: {
|
|
|
legend: {}
|
|
|
},
|
|
|
legendVisible: true,
|
|
|
activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
|
|
|
- // 核心修正:增加生命周期标识,防止组件销毁后异步回调继续执行
|
|
|
isComponentDestroyed: false,
|
|
|
drawSeq: 0,
|
|
|
driving: null,
|
|
|
infoCloseTimer: null,
|
|
|
activeInfoWindowId: null,
|
|
|
isInfoWindowHovered: false,
|
|
|
- // 状态类型配置
|
|
|
statusConfig: [
|
|
|
{ name: "中心计划", color: "#004CDE", type: "normal" },
|
|
|
{ name: "干线协调", color: "#13C373", type: "route" },
|
|
|
@@ -86,20 +82,16 @@ export default {
|
|
|
{ name: "降级", color: "#D9C13B", type: "abnormal" },
|
|
|
{ name: "故障", color: "#FF3938", type: "abnormal" }
|
|
|
],
|
|
|
- // 真实路口数据
|
|
|
intersectionData: [],
|
|
|
- // 按状态分类的路口数据
|
|
|
- statusIntersections: {},
|
|
|
-
|
|
|
+ statusIntersections: {}
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
|
- this.isComponentDestroyed = false; // 重置标识
|
|
|
+ this.isComponentDestroyed = false;
|
|
|
this.loadMapData().then(() => {
|
|
|
this.classifyIntersectionsByStatus();
|
|
|
this.updateMapByMode();
|
|
|
this.initAMap();
|
|
|
- // 在数据加载完成后存储坐标
|
|
|
this.storeStatusCoordsToLocalStorage();
|
|
|
});
|
|
|
|
|
|
@@ -188,7 +180,10 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
- // 动态加载地图数据
|
|
|
+ /**
|
|
|
+ * 动态加载地图数据
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ */
|
|
|
async loadMapData() {
|
|
|
try {
|
|
|
const mapDataModule = await import('@/mock/map_data_gaode.json');
|
|
|
@@ -200,16 +195,20 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 检查地图环境是否安全可用
|
|
|
+ /**
|
|
|
+ * 检查地图环境是否安全可用
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
isMapReady() {
|
|
|
return !this.isComponentDestroyed && this.map && typeof this.map.add === 'function';
|
|
|
},
|
|
|
|
|
|
- // 将真实路口数据按状态类型分类
|
|
|
+ /**
|
|
|
+ * 将真实路口数据按状态类型分类
|
|
|
+ */
|
|
|
classifyIntersectionsByStatus() {
|
|
|
const remainingData = this.intersectionData;
|
|
|
const maxAbnormalCount = 4;
|
|
|
- // 异常状态各取4个,剩余全部按比例分配给6个正常状态
|
|
|
const abnormalTotal = maxAbnormalCount * 3;
|
|
|
const normalData = remainingData.slice(0, remainingData.length - abnormalTotal);
|
|
|
const abnormalData = remainingData.slice(remainingData.length - abnormalTotal);
|
|
|
@@ -217,8 +216,8 @@ export default {
|
|
|
|
|
|
this.statusIntersections = {
|
|
|
"中心计划": normalData.slice(0, normalChunk),
|
|
|
- "干线协调": [], // 清空,改为动态生成
|
|
|
- "勤务路线": [], // 清空,改为动态生成
|
|
|
+ "干线协调": [],
|
|
|
+ "勤务路线": [],
|
|
|
"定周期控制": normalData.slice(normalChunk, normalChunk * 2),
|
|
|
"感应控制": normalData.slice(normalChunk * 2, normalChunk * 3),
|
|
|
"自适应控制": normalData.slice(normalChunk * 3, normalChunk * 4),
|
|
|
@@ -230,6 +229,9 @@ export default {
|
|
|
};
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据模式更新地图显示
|
|
|
+ */
|
|
|
updateMapByMode() {
|
|
|
switch (this.mode) {
|
|
|
case '路口':
|
|
|
@@ -246,6 +248,9 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 更新地图显示状态
|
|
|
+ */
|
|
|
updateMapDisplay() {
|
|
|
if (this.infoWindow) this.infoWindow.close();
|
|
|
if (!this.isMapReady()) return;
|
|
|
@@ -262,17 +267,19 @@ export default {
|
|
|
});
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 初始化高德地图
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ */
|
|
|
async initAMap() {
|
|
|
if (this.isComponentDestroyed) return;
|
|
|
|
|
|
- // 确保在加载前注入
|
|
|
window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
|
|
|
|
|
|
try {
|
|
|
const AMap = await AMapLoader.load({
|
|
|
key: this.amapKey,
|
|
|
version: "2.0",
|
|
|
- // 核心:确保 Driving 插件在此加载
|
|
|
plugins: ['AMap.Driving']
|
|
|
});
|
|
|
|
|
|
@@ -282,14 +289,11 @@ export default {
|
|
|
this.map = new AMap.Map(this.$refs.mapContainer, {
|
|
|
zoom: 15,
|
|
|
mapStyle: "amap://styles/darkblue",
|
|
|
- center: [116.663, 39.905], // 通州区中心
|
|
|
+ center: [116.663, 39.905]
|
|
|
});
|
|
|
|
|
|
this.driving = new AMap.Driving({ map: null, hideMarkers: true });
|
|
|
|
|
|
-
|
|
|
-
|
|
|
- // 建议在地图加载完成后再画线
|
|
|
this.map.on('complete', () => {
|
|
|
if (!this.isComponentDestroyed) {
|
|
|
this.drawStaticRoutes();
|
|
|
@@ -300,9 +304,10 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 绘制静态路线和标记
|
|
|
- // 对于普通状态,从 mock 数据加载路口标记
|
|
|
- // 对于路线类(干线协调和勤务路线),使用高德地图路线规划绘制路线和密集点位
|
|
|
+ /**
|
|
|
+ * 绘制静态路线和标记
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ */
|
|
|
async drawStaticRoutes() {
|
|
|
if (!this.isMapReady()) return;
|
|
|
|
|
|
@@ -321,11 +326,8 @@ export default {
|
|
|
{ start: [116.66325, 39.9171], end: [116.6833, 39.9171], color: "#13C373", trunkId: 'trunk_6', trunkName: '张台路与湖亦路路口' },
|
|
|
],
|
|
|
"勤务路线": [
|
|
|
- // 第一段:未执行(全红)— 通州大街西段,干线协调下方约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' }
|
|
|
]
|
|
|
};
|
|
|
@@ -333,7 +335,6 @@ export default {
|
|
|
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] || [];
|
|
|
const markers = intersections.map(item =>
|
|
|
@@ -348,7 +349,6 @@ export default {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- // 2. 处理路线类(干线/特勤)
|
|
|
this.routeGroups[config.name] = [];
|
|
|
const lines = realRouteConfigs[config.name] || [];
|
|
|
const trunkSegments = [];
|
|
|
@@ -372,7 +372,6 @@ export default {
|
|
|
path
|
|
|
});
|
|
|
|
|
|
- // 每条路线配置对应一条干线菜单项
|
|
|
if (config.name === '干线协调' && overlays.length > 0) {
|
|
|
const name = line.trunkName || ('干线' + (lineIdx + 1));
|
|
|
trunkSegments.push({
|
|
|
@@ -394,13 +393,15 @@ export default {
|
|
|
await this.sleep(80);
|
|
|
}
|
|
|
|
|
|
- // 干线协调绘制完成后,通知父组件用于菜单渲染
|
|
|
if (config.name === '干线协调' && trunkSegments.length > 0) {
|
|
|
this.$emit('bindTrunkMenuTree', trunkSegments);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 清除所有路线覆盖物
|
|
|
+ */
|
|
|
clearAllRouteOverlays() {
|
|
|
if (!this.isMapReady()) return;
|
|
|
try {
|
|
|
@@ -426,12 +427,24 @@ export default {
|
|
|
});
|
|
|
|
|
|
this.routeGroups = {};
|
|
|
+ this.dotMarkers = [];
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 休眠函数
|
|
|
+ * @param {number} ms - 休眠毫秒数
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ */
|
|
|
sleep(ms) {
|
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 带重试机制的路径规划
|
|
|
+ * @param {Array<number>} start - 起点坐标 [lng, lat]
|
|
|
+ * @param {Array<number>} end - 终点坐标 [lng, lat]
|
|
|
+ * @returns {Promise<Array>} 路径点数组
|
|
|
+ */
|
|
|
async searchDrivingPathWithRetry(start, end) {
|
|
|
if (!this.AMap || !this.driving || typeof this.driving.search !== 'function') {
|
|
|
throw new Error('Driving not ready');
|
|
|
@@ -450,6 +463,12 @@ export default {
|
|
|
throw lastErr || new Error('Driving search failed');
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 单次路径规划
|
|
|
+ * @param {Array<number>} start - 起点坐标 [lng, lat]
|
|
|
+ * @param {Array<number>} end - 终点坐标 [lng, lat]
|
|
|
+ * @returns {Promise<Array>} 路径点数组
|
|
|
+ */
|
|
|
searchDrivingPathOnce(start, end) {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
this.driving.search(start, end, (status, result) => {
|
|
|
@@ -468,6 +487,12 @@ export default {
|
|
|
});
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 为Promise添加超时机制
|
|
|
+ * @param {Promise} promise - 要执行的Promise
|
|
|
+ * @param {number} timeoutMs - 超时毫秒数
|
|
|
+ * @returns {Promise} 带超时的Promise
|
|
|
+ */
|
|
|
withTimeout(promise, timeoutMs) {
|
|
|
return Promise.race([
|
|
|
promise,
|
|
|
@@ -475,6 +500,16 @@ export default {
|
|
|
]);
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 从路径构建路线覆盖物
|
|
|
+ * @param {Object} params - 参数对象
|
|
|
+ * @param {Object} params.config - 配置对象
|
|
|
+ * @param {string} params.configName - 配置名称
|
|
|
+ * @param {Object} params.line - 路线配置
|
|
|
+ * @param {number} params.lineIdx - 路线索引
|
|
|
+ * @param {Array} params.path - 路径点数组
|
|
|
+ * @returns {Array} 覆盖物数组
|
|
|
+ */
|
|
|
buildRouteOverlaysFromPath({ config, configName, line, lineIdx, path }) {
|
|
|
if (!this.AMap || !this.map) return [];
|
|
|
|
|
|
@@ -483,8 +518,6 @@ export default {
|
|
|
basePath = this.buildFallbackLinePath(line.start, line.end, 30);
|
|
|
}
|
|
|
|
|
|
- // --- 核心优化:确保线路准确贴合真实道路 ---
|
|
|
- // 取消原本的微小物理解散偏移,以保证所有线路(包含但不限单条调整后)严格绘制在导航真实道路上
|
|
|
const offsetVal = 0;
|
|
|
const applyOffset = (p) => {
|
|
|
const lng = p.lng || (p.getLng ? p.getLng() : (Array.isArray(p) ? Number(p[0]) : 0));
|
|
|
@@ -493,7 +526,6 @@ export default {
|
|
|
};
|
|
|
|
|
|
const allSegments = this.extractMainStraightSegments(basePath);
|
|
|
- // 干线协调每条路线只取最长的1段,保证6条配置 = 6条线
|
|
|
const segments = configName === '干线协调'
|
|
|
? allSegments.slice(0, 1)
|
|
|
: allSegments;
|
|
|
@@ -502,10 +534,8 @@ export default {
|
|
|
segments.forEach((rawSegmentPath, segmentIdx) => {
|
|
|
if (!Array.isArray(rawSegmentPath) || rawSegmentPath.length < 2) return;
|
|
|
|
|
|
- // 应用偏移到当前段的所有点
|
|
|
const segmentPath = rawSegmentPath.map(p => applyOffset(p));
|
|
|
|
|
|
- // --- 勤务路线:根据 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);
|
|
|
@@ -711,6 +741,12 @@ export default {
|
|
|
return overlays;
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 均匀选取点的索引
|
|
|
+ * @param {number} totalPoints - 总点数
|
|
|
+ * @param {number} count - 要选取的点数
|
|
|
+ * @returns {Array<number>} 索引数组
|
|
|
+ */
|
|
|
pickEvenlySpacedIndices(totalPoints, count) {
|
|
|
const total = Math.max(Number(totalPoints) || 0, 0);
|
|
|
const target = Math.max(Number(count) || 0, 0);
|
|
|
@@ -721,6 +757,13 @@ export default {
|
|
|
return Array.from({ length: target }, (_, k) => Math.round((k * (total - 1)) / (target - 1)));
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 构建备选线路路径
|
|
|
+ * @param {Array<number>} start - 起点坐标 [lng, lat]
|
|
|
+ * @param {Array<number>} end - 终点坐标 [lng, lat]
|
|
|
+ * @param {number} pointCount - 点数量
|
|
|
+ * @returns {Array} 路径点数组
|
|
|
+ */
|
|
|
buildFallbackLinePath(start, end, pointCount) {
|
|
|
const sLng = Number(start && start[0]);
|
|
|
const sLat = Number(start && start[1]);
|
|
|
@@ -740,6 +783,11 @@ export default {
|
|
|
return path;
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 提取主要直线段
|
|
|
+ * @param {Array} path - 路径点数组
|
|
|
+ * @returns {Array} 直线段数组
|
|
|
+ */
|
|
|
extractMainStraightSegments(path) {
|
|
|
const getCoord = (p) => {
|
|
|
if (!p) return { lng: NaN, lat: NaN };
|
|
|
@@ -802,7 +850,11 @@ export default {
|
|
|
return finalSegments.length > 0 ? finalSegments : [points];
|
|
|
},
|
|
|
|
|
|
- // 内部通用工具:从不同格式的点中提取经纬度数组 [lng, lat]
|
|
|
+ /**
|
|
|
+ * 从不同格式的点中提取经纬度数组 [lng, lat]
|
|
|
+ * @param {Object|Array} p - 点对象或数组
|
|
|
+ * @returns {Array<number>} 经纬度数组
|
|
|
+ */
|
|
|
_getCoords(p) {
|
|
|
if (!p) return [0, 0];
|
|
|
const lng = p.lng || (p.getLng ? p.getLng() : (Array.isArray(p) ? Number(p[0]) : 0));
|
|
|
@@ -810,6 +862,12 @@ export default {
|
|
|
return [lng, lat];
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 计算两点之间的方位角(度)
|
|
|
+ * @param {Object|Array} a - 起点
|
|
|
+ * @param {Object|Array} b - 终点
|
|
|
+ * @returns {number} 方位角
|
|
|
+ */
|
|
|
calcBearingDeg(a, b) {
|
|
|
const [alng, alat] = this._getCoords(a);
|
|
|
const [blng, blat] = this._getCoords(b);
|
|
|
@@ -822,12 +880,24 @@ export default {
|
|
|
return (90 - mathAngle + 360) % 360;
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 计算两个角度的差值
|
|
|
+ * @param {number} a - 角度1
|
|
|
+ * @param {number} b - 角度2
|
|
|
+ * @returns {number} 角度差
|
|
|
+ */
|
|
|
calcAngleDiffDeg(a, b) {
|
|
|
let diff = Math.abs(a - b);
|
|
|
if (diff > 180) diff = 360 - diff;
|
|
|
return diff;
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 计算两点之间的近似距离
|
|
|
+ * @param {Object|Array} a - 点1
|
|
|
+ * @param {Object|Array} b - 点2
|
|
|
+ * @returns {number} 距离
|
|
|
+ */
|
|
|
calcApproxDistance(a, b) {
|
|
|
const [alng, alat] = this._getCoords(a);
|
|
|
const [blng, blat] = this._getCoords(b);
|
|
|
@@ -838,6 +908,46 @@ export default {
|
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据当前缩放级别计算普通圆点的尺寸(px)
|
|
|
+ * @returns {number} 圆点尺寸
|
|
|
+ */
|
|
|
+ getDotSizeByZoom() {
|
|
|
+ if (!this.map) return 14;
|
|
|
+ const zoom = this.map.getZoom();
|
|
|
+ const base = 14;
|
|
|
+ const extra = Math.max(0, zoom - 15) * 3;
|
|
|
+ return Math.min(base + extra, 28);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 遍历所有已注册的普通圆点 marker,更新其 content 和 offset
|
|
|
+ */
|
|
|
+ updateDotMarkerSizes() {
|
|
|
+ if (!this.isMapReady()) return;
|
|
|
+ const size = this.getDotSizeByZoom();
|
|
|
+ const half = Math.round(size / 2) + 2;
|
|
|
+ this.dotMarkers.forEach(({ marker, config, isRoute }) => {
|
|
|
+ if (!marker || typeof marker.setContent !== 'function') return;
|
|
|
+ const displayText = config.name ? config.name.charAt(0) : '';
|
|
|
+ const border = isRoute ? 'none' : '1.5px solid rgba(255,255,255,0.7)';
|
|
|
+ const boxShadow = isRoute ? 'none' : `0 0 8px ${config.color}`;
|
|
|
+ marker.setContent(`
|
|
|
+ <div class="pure-light-node ${isRoute ? 'route-node' : ''}" style="width:${size}px;height:${size}px;background:${config.color || '#999'};box-shadow:${boxShadow};border:${border};box-sizing:content-box;display:flex;justify-content:center;align-items:center;color:#fff;border-radius:50%;cursor:pointer;padding:2px;">
|
|
|
+ <span style="transform:scale(0.8);font-weight:bold;font-size:${Math.max(10, size - 2)}px;">${displayText}</span>
|
|
|
+ </div>
|
|
|
+ `);
|
|
|
+ marker.setOffset(new this.AMap.Pixel(-half, -half));
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建交通信号灯标记
|
|
|
+ * @param {Array<number>|Object} position - 位置坐标
|
|
|
+ * @param {Object} config - 配置对象
|
|
|
+ * @param {string} [type='normal'] - 标记类型
|
|
|
+ * @returns {Object|null} 标记对象
|
|
|
+ */
|
|
|
createTrafficLightMarker(position, config, type = 'normal') {
|
|
|
if (!position || !config) return null;
|
|
|
|
|
|
@@ -846,7 +956,6 @@ export default {
|
|
|
const lat = position.getLat ? position.getLat() : Number(position[1] !== undefined ? position[1] : position.lat);
|
|
|
if (isNaN(lng) || isNaN(lat)) return null;
|
|
|
|
|
|
- // 状态文字:起、终、或者状态配置的首字母
|
|
|
let displayText = config.name ? config.name.charAt(0) : '';
|
|
|
if (type === 'start') displayText = '起';
|
|
|
if (type === 'end') displayText = '终';
|
|
|
@@ -856,7 +965,6 @@ export default {
|
|
|
const isStartEnd = type === 'start' || type === 'end';
|
|
|
const isPassed = type === 'passed';
|
|
|
|
|
|
- // 核心配置映射:减少嵌套逻辑
|
|
|
const markerStyle = isStartEnd ? {
|
|
|
size: '24px',
|
|
|
height: '30px',
|
|
|
@@ -870,14 +978,13 @@ export default {
|
|
|
zIndex: 110,
|
|
|
border: 'none'
|
|
|
} : {
|
|
|
- size: '14px',
|
|
|
- height: '14px',
|
|
|
- offset: [-9, -9], // 14px + 2px padding * 2 = 18px total
|
|
|
+ size: '18px',
|
|
|
+ height: '18px',
|
|
|
+ offset: [-11, -11],
|
|
|
zIndex: 100,
|
|
|
border: isRoute ? 'none' : '1.5px solid rgba(255,255,255,0.7)'
|
|
|
});
|
|
|
|
|
|
- // 生成标记内容
|
|
|
let markerContent = '';
|
|
|
if (isStartEnd) {
|
|
|
markerContent = `
|
|
|
@@ -901,7 +1008,7 @@ export default {
|
|
|
} else {
|
|
|
markerContent = `
|
|
|
<div class="pure-light-node ${isRoute ? 'route-node' : ''}" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: ${config.color || '#999'}; box-shadow: ${isRoute ? 'none' : `0 0 8px ${config.color}`}; border: ${markerStyle.border}; box-sizing: content-box; display: flex; justify-content: center; align-items: center; color: #fff; border-radius: 50%; cursor: pointer; padding: 2px;">
|
|
|
- <span style="transform: scale(0.8); font-weight: bold; font-size: 12px;">${displayText}</span>
|
|
|
+ <span style="transform: scale(0.8); font-weight: bold; font-size: 14px;">${displayText}</span>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
@@ -922,6 +1029,11 @@ export default {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ if (!isStartEnd && !isAbnormal && !isPassed) {
|
|
|
+ const isRoute = ["干线协调", "勤务路线"].includes(config.name);
|
|
|
+ this.dotMarkers.push({ marker, config, isRoute });
|
|
|
+ }
|
|
|
+
|
|
|
marker.on('click', (e) => {
|
|
|
if (this.isComponentDestroyed) return;
|
|
|
const extData = e.target.getExtData();
|
|
|
@@ -929,7 +1041,6 @@ export default {
|
|
|
this.cancelCloseInfoWindow();
|
|
|
this.openLightInfo(extData, e.lnglat);
|
|
|
}
|
|
|
- // 获取像素坐标
|
|
|
const pixel = this.map.lngLatToContainer(e.lnglat);
|
|
|
this.$emit('map-crossing-click', extData, e.lnglat, pixel);
|
|
|
});
|
|
|
@@ -940,9 +1051,7 @@ export default {
|
|
|
this.cancelCloseInfoWindow();
|
|
|
this.openLightInfo(e.target.getExtData(), e.lnglat);
|
|
|
}
|
|
|
- // 获取像素坐标
|
|
|
const pixel = this.map.lngLatToContainer(e.lnglat);
|
|
|
- // 传递路口鼠标滑入事件
|
|
|
this.$emit('map-crossing-mouseover', e.target.getExtData(), e.lnglat, pixel);
|
|
|
});
|
|
|
|
|
|
@@ -951,7 +1060,6 @@ export default {
|
|
|
if (this.$route && this.$route.path === '/home') {
|
|
|
this.scheduleCloseInfoWindow();
|
|
|
}
|
|
|
- // 传递路口鼠标滑出事件
|
|
|
this.$emit('map-crossing-mouseout', e.target.getExtData());
|
|
|
});
|
|
|
|
|
|
@@ -962,6 +1070,11 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 打开信号灯信息窗口
|
|
|
+ * @param {Object} data - 信号灯数据
|
|
|
+ * @param {Array<number>} position - 位置坐标
|
|
|
+ */
|
|
|
openLightInfo(data, position) {
|
|
|
if (!this.isMapReady()) return;
|
|
|
|
|
|
@@ -1010,7 +1123,7 @@ export default {
|
|
|
this.infoWindow = new this.AMap.InfoWindow({
|
|
|
isCustom: true,
|
|
|
offset: new this.AMap.Pixel(0, -20),
|
|
|
- autoMove: false // 防止弹窗自动平移导致的中心点偏移
|
|
|
+ autoMove: false
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -1042,6 +1155,9 @@ export default {
|
|
|
}, 100);
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 取消关闭信息窗口
|
|
|
+ */
|
|
|
cancelCloseInfoWindow() {
|
|
|
if (this.infoCloseTimer) {
|
|
|
clearTimeout(this.infoCloseTimer);
|
|
|
@@ -1049,6 +1165,9 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 安排关闭信息窗口
|
|
|
+ */
|
|
|
scheduleCloseInfoWindow() {
|
|
|
this.cancelCloseInfoWindow();
|
|
|
this.infoCloseTimer = setTimeout(() => {
|
|
|
@@ -1059,6 +1178,11 @@ export default {
|
|
|
}, 160);
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取报警信息文本
|
|
|
+ * @param {string} statusName - 状态名称
|
|
|
+ * @returns {string} 报警信息
|
|
|
+ */
|
|
|
getAlarmInfoText(statusName) {
|
|
|
if (statusName === '离线') return '通讯中断设备离线';
|
|
|
if (statusName === '降级') return '降级定周期控制';
|
|
|
@@ -1066,6 +1190,11 @@ export default {
|
|
|
return '设备异常';
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 格式化事件时间
|
|
|
+ * @param {Date|number|string} input - 时间输入
|
|
|
+ * @returns {string} 格式化后的时间
|
|
|
+ */
|
|
|
formatEventTime(input) {
|
|
|
let d = null;
|
|
|
if (input instanceof Date) d = input;
|
|
|
@@ -1083,6 +1212,9 @@ export default {
|
|
|
return `${y}.${m}.${day} ${hh}:${mm}`;
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 切换所有图例的可见性
|
|
|
+ */
|
|
|
toggleAll() {
|
|
|
const targetState = !this.isAllSelected;
|
|
|
if (!this.isMapReady()) return;
|
|
|
@@ -1101,6 +1233,10 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 切换指定路线的可见性
|
|
|
+ * @param {string} name - 路线名称
|
|
|
+ */
|
|
|
toggleRouteVisible(name) {
|
|
|
if (!this.isMapReady()) return;
|
|
|
|
|
|
@@ -1116,10 +1252,13 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据位置聚焦地图
|
|
|
+ * @param {string|Array<number>} targetPos - 目标位置
|
|
|
+ */
|
|
|
focusByLocation(targetPos) {
|
|
|
if (!this.isMapReady() || !targetPos) return;
|
|
|
|
|
|
- // 如果是字符串坐标 "lng,lat",则解析
|
|
|
let pos = targetPos;
|
|
|
if (typeof targetPos === 'string') {
|
|
|
pos = targetPos.split(',').map(Number);
|
|
|
@@ -1130,7 +1269,6 @@ export default {
|
|
|
let bestMarker = null;
|
|
|
let minDistanceSq = Infinity;
|
|
|
|
|
|
- // 遍历所有路由组,寻找离坐标点最近的标记
|
|
|
Object.values(this.routeGroups).forEach(group => {
|
|
|
if (!Array.isArray(group)) return;
|
|
|
group.forEach(item => {
|
|
|
@@ -1143,9 +1281,7 @@ export default {
|
|
|
const dy = markerPos[1] - targetLat;
|
|
|
const distSq = dx * dx + dy * dy;
|
|
|
|
|
|
- // 容差范围内(约 20 米),寻找最接近的点
|
|
|
if (distSq < 0.000001) {
|
|
|
- // 优先规则:如果距离相同或非常接近,优先选择异常状态点(离线/降级/故障)
|
|
|
const isAbnormal = ["离线", "降级", "故障"].includes(markerExt.name);
|
|
|
const currentIsAbnormal = bestMarker ? ["离线", "降级", "故障"].includes(bestMarker.getExtData().name) : false;
|
|
|
|
|
|
@@ -1164,12 +1300,15 @@ export default {
|
|
|
if (!this.isComponentDestroyed) this.openLightInfo(bestMarker.getExtData(), finalPos);
|
|
|
}, 600);
|
|
|
} else {
|
|
|
- // 如果找不到对应的标记,直接使用传入的坐标设置地图中心
|
|
|
this.map.setZoomAndCenter(17, [targetLng, targetLat], false, 500);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 通过路口编号定位到对应的 marker,弹出信息窗
|
|
|
+ /**
|
|
|
+ * 通过路口编号定位到对应的标记
|
|
|
+ * @param {string} id - 路口编号
|
|
|
+ * @returns {Array<number>|null} 位置坐标
|
|
|
+ */
|
|
|
focusById(id) {
|
|
|
if (!this.isMapReady() || !id) return null;
|
|
|
|
|
|
@@ -1194,11 +1333,16 @@ export default {
|
|
|
return null;
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 切换图例的可见性
|
|
|
+ */
|
|
|
toggleLegend() {
|
|
|
this.legendVisible = !this.legendVisible;
|
|
|
},
|
|
|
|
|
|
- // 按4:4:4比例提取故障、离线、降级路口信息并存储到localStorage
|
|
|
+ /**
|
|
|
+ * 按4:4:4比例提取故障、离线、降级路口信息并存储到localStorage
|
|
|
+ */
|
|
|
storeStatusCoordsToLocalStorage() {
|
|
|
const alarmTypes = [
|
|
|
{ status: "故障", titles: ["通讯中断", "灯组故障", "相位冲突", "绿冲突"], level: "high", type: "error" },
|
|
|
@@ -1224,7 +1368,6 @@ export default {
|
|
|
description: `${name}-${titles[i] || titles[0]}`,
|
|
|
position: [lng, lat],
|
|
|
});
|
|
|
- // 保持原有的 pos1-pos12 存储,兼容其他可能的引用
|
|
|
localStorage.setItem(`pos${id}`, `${lng},${lat}`);
|
|
|
id++;
|
|
|
});
|
|
|
@@ -1233,7 +1376,13 @@ export default {
|
|
|
localStorage.setItem("alarmListFromMap", JSON.stringify(alarmList));
|
|
|
console.log('状态坐标及告警数据已存储到localStorage');
|
|
|
},
|
|
|
- // 公开方法:将经纬度转换为像素坐标
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将经纬度转换为像素坐标
|
|
|
+ * @param {number} lng - 经度
|
|
|
+ * @param {number} lat - 纬度
|
|
|
+ * @returns {Object|null} 像素坐标
|
|
|
+ */
|
|
|
lngLatToPixel(lng, lat) {
|
|
|
if (!this.map) return null;
|
|
|
return this.map.lngLatToContainer([lng, lat]);
|
|
|
@@ -1614,30 +1763,10 @@ export default {
|
|
|
z-index: 1;
|
|
|
}
|
|
|
|
|
|
-/* 异常状态增加稍微剧烈一点的呼吸感,但缩小范围 */
|
|
|
-@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);
|
|
|
}
|
|
|
|
|
|
-/* 勤务路线执行进度点脉冲动画 */
|
|
|
::v-deep .duty-progress-node {
|
|
|
animation: duty-pulse 1.6s infinite ease-in-out;
|
|
|
}
|