|
|
@@ -2,22 +2,25 @@
|
|
|
<div class="map-wrapper">
|
|
|
<div ref="mapContainer" class="map-container"></div>
|
|
|
|
|
|
- <div class="map-header" v-if="initialized" :style="privateStyle.search">
|
|
|
- <div class="search-form">
|
|
|
- <input type="text" v-model="searchQuery" placeholder="请输入路段或设备名称" class="search-input"
|
|
|
- @keyup.enter="handleSearch" />
|
|
|
- <button class="search-btn" @click="handleSearch">查询</button>
|
|
|
- </div>
|
|
|
- <div class="action-box" @click="toggleAll">全选 ▾</div>
|
|
|
+ <div class="map-search-bar" :style="privateStyle.search">
|
|
|
+ <input v-model="searchKey" type="text" placeholder="请输入路段或设备名称" @keyup.enter="handleSearch" />
|
|
|
+ <button @click="handleSearch">查询</button>
|
|
|
</div>
|
|
|
|
|
|
- <div class="map-legend" v-if="initialized" :style="privateStyle.legend">
|
|
|
+ <div class="map-legend" :style="privateStyle.legend">
|
|
|
<div class="legend-title">图例</div>
|
|
|
<div class="legend-list">
|
|
|
- <div v-for="item in legendConfig" :key="item.type" class="legend-item"
|
|
|
- :class="{ 'is-hidden': !activeLegends.includes(item.type) }" @click="handleLegendClick(item.type)">
|
|
|
- <span class="legend-dot" :style="{ backgroundColor: item.color }"></span>
|
|
|
- <span class="legend-label">{{ item.label }}</span>
|
|
|
+ <div class="legend-item all-select" @click="toggleAll">
|
|
|
+ <div class="legend-dot"
|
|
|
+ :style="{ backgroundColor: isAllSelected ? '#fff' : 'transparent', border: '1px solid #fff' }"></div>
|
|
|
+ <div class="legend-label" style="font-weight: bold;">全选</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-for="item in legendConfig" :key="item.name" class="legend-item"
|
|
|
+ :class="{ 'is-inactive': !activeLegends.includes(item.name) }" @click="toggleRouteVisible(item.name)">
|
|
|
+ <div class="legend-dot" :style="{ backgroundColor: item.color }"></div>
|
|
|
+ <div class="legend-label">{{ item.name }}</div>
|
|
|
+ <!-- <div class="legend-status">{{ activeLegends.includes(item.name) ? '在线' : '离线' }}</div> -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -37,30 +40,30 @@ export default {
|
|
|
return {
|
|
|
AMap: null,
|
|
|
map: null,
|
|
|
- drivingInstances: [], // 存储路径规划实例
|
|
|
infoWindow: null,
|
|
|
- initialized: false,
|
|
|
- searchQuery: '', // 搜索内容绑定
|
|
|
- activeLegends: [],
|
|
|
- legendConfig: [
|
|
|
- { type: 'all_selected', label: '全选', color: '#e5e5e5' },
|
|
|
- { type: 'center_plan', label: '中心计划', color: '#32c5ff' },
|
|
|
- { type: 'trunk_coord', label: '干线协调', color: '#3ee68d' },
|
|
|
- { type: 'service_route', label: '勤务路线', color: '#ffcc33' },
|
|
|
- { type: 'periodic', label: '定周期控制', color: '#00ccff' },
|
|
|
- { type: 'induction', label: '感应控制', color: '#7ed3b2' },
|
|
|
- { type: 'adaptive', label: '自适应控制', color: '#8ca1ff' },
|
|
|
- { type: 'manual', label: '手动控制', color: '#cc8d66' },
|
|
|
- { type: 'special', label: '特殊控制', color: '#ffb33b' },
|
|
|
- { type: 'offline', label: '离线', color: '#6d7791' },
|
|
|
- { type: 'degraded', label: '降级', color: '#c4a737' },
|
|
|
- { type: 'fault', label: '故障', color: '#ff4d4f' }
|
|
|
- ],
|
|
|
- overlayGroups: {},
|
|
|
+ routeGroups: {},
|
|
|
+ polylines: [],
|
|
|
+ searchKey: '',
|
|
|
+ placeSearch: null,
|
|
|
privateStyle: {
|
|
|
search: {},
|
|
|
legend: {}
|
|
|
- }
|
|
|
+ },
|
|
|
+ activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
|
|
|
+ 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.8850], color: "#FF3938" } // 潞通大街
|
|
|
+ ]
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
|
@@ -68,188 +71,217 @@ export default {
|
|
|
|
|
|
// 自定义首页地图搜索和图例位置样式
|
|
|
if (this.$route.path === '/home') {
|
|
|
- this.privateStyle.search = { top: "100px", left: "25%", right: "25%" };
|
|
|
+ this.privateStyle.search = { top: "100px", left: "25%" };
|
|
|
this.privateStyle.legend = { right: "25%" };
|
|
|
}
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- if (this.map) {
|
|
|
- this.map.destroy();
|
|
|
+ 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();
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 判断是否所有图例都在激活列表中
|
|
|
+ isAllSelected() {
|
|
|
+ return this.activeLegends.length === this.legendConfig.length;
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
+ // 修改后的 initAMap
|
|
|
async initAMap() {
|
|
|
- // 1. 配置安全密钥(必须在 load 之前)
|
|
|
- window._AMapSecurityConfig = {
|
|
|
- securityJsCode: this.securityJsCode,
|
|
|
- };
|
|
|
-
|
|
|
+ window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
|
|
|
try {
|
|
|
- // 2. 加载地图核心及插件
|
|
|
- const AMap = await AMapLoader.load({
|
|
|
+ this.AMap = await AMapLoader.load({
|
|
|
key: this.amapKey,
|
|
|
version: "2.0",
|
|
|
- plugins: ['AMap.Driving']
|
|
|
+ plugins: ['AMap.Driving', 'AMap.GeometryUtil', 'AMap.PlaceSearch'] // 必须有这个
|
|
|
});
|
|
|
|
|
|
- this.AMap = AMap;
|
|
|
-
|
|
|
- // 3. 实例化地图
|
|
|
- this.map = new AMap.Map(this.$refs.mapContainer, {
|
|
|
+ this.map = new this.AMap.Map(this.$refs.mapContainer, {
|
|
|
zoom: 14,
|
|
|
mapStyle: "amap://styles/darkblue",
|
|
|
- viewMode: "3D",
|
|
|
- center: [116.661132, 39.902996], // 通州火车站中心
|
|
|
+ center: [116.663, 39.905],
|
|
|
});
|
|
|
|
|
|
- // 4. 等待地图加载完成后执行路径规划
|
|
|
- this.map.on('complete', () => {
|
|
|
- console.log("地图加载完成,开始绘制真实路网...");
|
|
|
- this.initialized = true;
|
|
|
- this.activeLegends = this.legendConfig.map(l => l.type);
|
|
|
- this.drawStaticRoutes();
|
|
|
+ // 关键:在这里初始化,确保 search 方法可用
|
|
|
+ this.placeSearch = new this.AMap.PlaceSearch({
|
|
|
+ map: this.map,
|
|
|
+ pageSize: 1,
|
|
|
+ autoFitView: true
|
|
|
+ });
|
|
|
+
|
|
|
+ this.map.on('complete', () => this.drawStaticRoutes());
|
|
|
+ } catch (err) { console.error('地图初始化失败', err); }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 搜索功能实现
|
|
|
+ handleSearch() {
|
|
|
+ if (!this.searchKey) return;
|
|
|
+
|
|
|
+ // 1. 逻辑优先:搜索本地设备名称
|
|
|
+ const foundLegend = this.legendConfig.find(item =>
|
|
|
+ item.name.includes(this.searchKey)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (foundLegend) {
|
|
|
+ // 如果搜到了图例中的路段,直接定位到该路段起点并打开图例
|
|
|
+ if (!this.activeLegends.includes(foundLegend.name)) {
|
|
|
+ this.toggleRouteVisible(foundLegend.name);
|
|
|
+ }
|
|
|
+ this.map.setZoomAndCenter(15, foundLegend.start);
|
|
|
+ } else {
|
|
|
+ // 2. 逻辑兜底:调用高德地点搜索 API
|
|
|
+ this.placeSearch.search(this.searchKey, (status, result) => {
|
|
|
+ console.log('result => ', result);
|
|
|
+ if (status !== 'complete') {
|
|
|
+ console.warn('未找到相关位置');
|
|
|
+ }
|
|
|
});
|
|
|
- } catch (err) {
|
|
|
- console.error('地图加载失败:', err);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
drawStaticRoutes() {
|
|
|
- // 定义 3横 3纵 坐标(确保起终点大致跨越通州核心区,让 Driving 自动找路)
|
|
|
- const routeConfigs = [
|
|
|
- // --- 横向 3 条 ---
|
|
|
- { name: "新华大街", start: [116.642, 39.910], end: [116.680, 39.911], color: "#32c5ff" },
|
|
|
- { name: "玉带河大街", start: [116.642, 39.902], end: [116.680, 39.903], color: "#3ee68d" },
|
|
|
- { name: "运河西大街", start: [116.642, 39.894], end: [116.680, 39.895], color: "#ffcc33" },
|
|
|
- // --- 纵向 3 条 ---
|
|
|
- { name: "车站路", start: [116.652, 39.915], end: [116.653, 39.889], color: "#fc8c23" },
|
|
|
- { name: "新华南路", start: [116.661, 39.916], end: [116.661, 39.889], color: "#8ca1ff" },
|
|
|
- { name: "东关大道", start: [116.671, 39.916], end: [116.671, 39.890], color: "#cc8d66" }
|
|
|
- ];
|
|
|
+ const AMap = this.AMap;
|
|
|
|
|
|
- routeConfigs.forEach(config => {
|
|
|
- // 为每一条路创建一个 Driving 实例
|
|
|
- const driving = new this.AMap.Driving({
|
|
|
- map: this.map, // 直接展现结果在地图上
|
|
|
- hideMarkers: false, // 显示起终点 Marker
|
|
|
- autoFitView: false, // 禁止自动缩放(防止多条线时地图乱跳)
|
|
|
- outlineColor: '#000', // 路线描边
|
|
|
- // 默认样式是蓝色,由于 Driving.search 不直接支持自定义颜色,
|
|
|
- // 这里我们先渲染默认路线。
|
|
|
+ this.legendConfig.forEach((config) => {
|
|
|
+ const driving = new AMap.Driving({
|
|
|
+ map: null,
|
|
|
+ hideMarkers: true,
|
|
|
+ autoFitView: false
|
|
|
});
|
|
|
|
|
|
- // 执行搜索
|
|
|
driving.search(config.start, config.end, (status, result) => {
|
|
|
- if (status === 'complete') {
|
|
|
- console.log(`${config.name} 绘制成功`);
|
|
|
+ let markers = [];
|
|
|
+ let path = [];
|
|
|
+
|
|
|
+ if (status === 'complete' && result.routes[0]) {
|
|
|
+ // --- 情况 A: 规划成功,获取精确路网线条 ---
|
|
|
+ const route = result.routes[0];
|
|
|
+ route.steps.forEach(step => { path = path.concat(step.path); });
|
|
|
} else {
|
|
|
- console.error(`${config.name} 绘制失败:`, result);
|
|
|
+ // --- 情况 B: 规划失败,使用保底策略(直接连接起终点) ---
|
|
|
+ console.warn(`${config.name} 路径规划失败,切换为保底直线模式`);
|
|
|
+ path = [config.start, config.end];
|
|
|
}
|
|
|
- });
|
|
|
|
|
|
- // this.drivingInstances.push(driving);
|
|
|
- });
|
|
|
- },
|
|
|
+ // 1. 统一绘制路线(无论是精确路网还是直线)
|
|
|
+ const polyline = new AMap.Polyline({
|
|
|
+ path: path,
|
|
|
+ strokeColor: config.color,
|
|
|
+ strokeWeight: 8,
|
|
|
+ strokeOpacity: 0.8,
|
|
|
+ showDir: true,
|
|
|
+ map: this.map
|
|
|
+ });
|
|
|
|
|
|
- // 搜索查询逻辑
|
|
|
- handleSearch() {
|
|
|
- if (!this.searchQuery.trim()) {
|
|
|
- alert("请输入查询内容");
|
|
|
- return;
|
|
|
- }
|
|
|
- console.log("正在执行搜索:", this.searchQuery);
|
|
|
- // 这里可以扩展具体的搜索逻辑,比如搜索 POI 或在已有 overlayGroups 中高亮匹配项
|
|
|
- alert(`已提交查询:${this.searchQuery}`);
|
|
|
- },
|
|
|
+ // 2. 在路径上分布点
|
|
|
+ // 如果只有两个点(直线),就只取起点和终点;如果有路网,取起中终
|
|
|
+ const points = path.length > 2
|
|
|
+ ? [path[0], path[Math.floor(path.length / 2)], path[path.length - 1]]
|
|
|
+ : [path[0], path[1]];
|
|
|
|
|
|
- drawTrafficScene() {
|
|
|
- const roads = [
|
|
|
- { type: 'center_plan', name: '新华南北路 - 中心控制段', path: [[116.665, 39.940], [116.665, 39.885]], hasDots: true },
|
|
|
- { type: 'trunk_coord', name: '通胡大街 - 干线协调(北)', path: [[116.620, 39.930], [116.650, 39.920], [116.700, 39.930]] },
|
|
|
- { type: 'trunk_coord', name: '运河东大街 - 干线协调(南)', path: [[116.615, 39.900], [116.660, 39.910], [116.710, 39.900]] },
|
|
|
- { type: 'service_route', name: '新华东街 - 勤务专用线', path: [[116.625, 39.905], [116.700, 39.905]] },
|
|
|
- { type: 'fault', name: '路县故城周边 - 设备异常', path: [[116.685, 39.920], [116.690, 39.905]] }
|
|
|
- ];
|
|
|
- roads.forEach(road => this.renderRoadWithStyle(road));
|
|
|
- },
|
|
|
-
|
|
|
- renderRoadWithStyle(road) {
|
|
|
- const config = this.legendConfig.find(l => l.type === road.type);
|
|
|
- const group = [];
|
|
|
+ points.forEach(pos => {
|
|
|
+ markers.push(this.createTrafficLightMarker(pos, config));
|
|
|
+ });
|
|
|
|
|
|
- const glow = new this.AMap.Polyline({
|
|
|
- path: road.path, strokeColor: config.color, strokeOpacity: 0.15, strokeWeight: 20, map: this.map
|
|
|
- });
|
|
|
+ // 3. 捆绑入组
|
|
|
+ const group = new AMap.OverlayGroup([...markers, polyline]);
|
|
|
+ this.map.add(group);
|
|
|
+ this.$set(this.routeGroups, config.name, group);
|
|
|
|
|
|
- const line = new this.AMap.Polyline({
|
|
|
- path: road.path, strokeColor: config.color, strokeWeight: 6, map: this.map
|
|
|
+ // 初始化显隐
|
|
|
+ if (!this.activeLegends.includes(config.name)) group.hide();
|
|
|
+ });
|
|
|
});
|
|
|
+ },
|
|
|
|
|
|
- [glow, line].forEach(item => {
|
|
|
- item.on('click', (e) => this.openDetailWindow(road, e.lnglat));
|
|
|
- group.push(item);
|
|
|
+ // 创建交通灯点
|
|
|
+ createTrafficLightMarker(position, config) {
|
|
|
+ console.log(config.name);
|
|
|
+ // Mock 红绿灯实时数据
|
|
|
+ const states = [
|
|
|
+ { color: '#ff4d4f', label: '红灯锁定', code: 'RED' },
|
|
|
+ { color: '#3ee68d', label: '绿灯通行', code: 'GREEN' },
|
|
|
+ { color: '#ffcc33', label: '黄灯警示', code: 'YELLOW' }
|
|
|
+ ];
|
|
|
+ const currentState = states[Math.floor(Math.random() * states.length)];
|
|
|
+ const countdown = Math.floor(Math.random() * 50) + 10;
|
|
|
+
|
|
|
+ const marker = new this.AMap.Marker({
|
|
|
+ position: position,
|
|
|
+ // 去掉了数字显示,仅保留呼吸灯效果
|
|
|
+ content: `<div class="pure-light-node" style="background: ${config.color}; box-shadow: 0 0 15px ${config.color}; font-size: 12px; display: flex; justify-content: center; align-items: center; color: #fff;">${config?.name?.charAt(0)}</div>`,
|
|
|
+ offset: new this.AMap.Pixel(-10, -10),
|
|
|
+ extData: {
|
|
|
+ ...config,
|
|
|
+ lightDetail: {
|
|
|
+ status: currentState.label,
|
|
|
+ color: currentState.color,
|
|
|
+ timeLeft: countdown,
|
|
|
+ sn: 'TL-' + Math.random().toString(36).substr(2, 7).toUpperCase()
|
|
|
+ }
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
- if (road.hasDots) {
|
|
|
- const dots = this.calculatePathDots(road.path, 12);
|
|
|
- dots.forEach((pos, index) => {
|
|
|
- const marker = new this.AMap.Marker({
|
|
|
- position: pos,
|
|
|
- content: `<div class="pulse-dot"></div>`,
|
|
|
- offset: new this.AMap.Pixel(-6, -6),
|
|
|
- map: this.map
|
|
|
- });
|
|
|
- marker.on('click', (e) => this.openDetailWindow({ ...road, name: `${road.name}-监测点${index + 1}` }, e.lnglat));
|
|
|
- group.push(marker);
|
|
|
- });
|
|
|
- }
|
|
|
- this.overlayGroups[road.type] = (this.overlayGroups[road.type] || []).concat(group);
|
|
|
+ marker.on('click', (e) => this.openLightInfo(e.target.getExtData(), e.lnglat));
|
|
|
+ return marker;
|
|
|
},
|
|
|
|
|
|
- calculatePathDots(path, count) {
|
|
|
- const p1 = path[0], p2 = path[1];
|
|
|
- const dots = [];
|
|
|
- for (let i = 1; i < count; i++) {
|
|
|
- dots.push([p1[0] + (p2[0] - p1[0]) * (i / count), p1[1] + (p2[1] - p1[1]) * (i / count)]);
|
|
|
- }
|
|
|
- return dots;
|
|
|
- },
|
|
|
-
|
|
|
- openDetailWindow(data, lnglat) {
|
|
|
- const config = this.legendConfig.find(l => l.type === data.type);
|
|
|
+ openLightInfo(data, position) {
|
|
|
const content = `
|
|
|
- <div class="custom-info-card">
|
|
|
- <div class="card-header" style="background: ${config.color}22; border-left: 4px solid ${config.color}">
|
|
|
- <span class="title">${data.name}</span>
|
|
|
+ <div class="traffic-window">
|
|
|
+ <div class="window-header" style="background: ${data.lightDetail.color}">
|
|
|
+ <span class="title">路口信号机: ${data.name}</span>
|
|
|
</div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="info-row"><span class="label">管控类型:</span><span class="value" style="color:${config.color}">${config.label}</span></div>
|
|
|
- <div class="info-row"><span class="label">当前状态:</span><span class="value" style="color:#3ee68d">运行中</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, lnglat);
|
|
|
+ this.infoWindow.open(this.map, position);
|
|
|
},
|
|
|
|
|
|
- handleLegendClick(type) {
|
|
|
- if (type === 'all_selected') return this.toggleAll();
|
|
|
- const isVisible = this.activeLegends.includes(type);
|
|
|
- this.activeLegends = isVisible ? this.activeLegends.filter(t => t !== type) : [...this.activeLegends, type];
|
|
|
- const group = this.overlayGroups[type];
|
|
|
- if (group) group.forEach(o => isVisible ? o.hide() : o.show());
|
|
|
- if (isVisible) this.infoWindow.close();
|
|
|
+ // 全选/全不选逻辑
|
|
|
+ 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 allTypes = this.legendConfig.map(l => l.type);
|
|
|
- const isShowingAll = this.activeLegends.length === allTypes.length;
|
|
|
- allTypes.forEach(type => {
|
|
|
- const group = this.overlayGroups[type];
|
|
|
- if (group) group.forEach(o => isShowingAll ? o.hide() : o.show());
|
|
|
- });
|
|
|
- this.activeLegends = isShowingAll ? [] : allTypes;
|
|
|
- this.infoWindow.close();
|
|
|
+ // 保留你原有的单个切换方法,但确保逻辑一致
|
|
|
+ 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();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -257,12 +289,10 @@ export default {
|
|
|
|
|
|
<style scoped>
|
|
|
.map-wrapper {
|
|
|
- position: relative;
|
|
|
width: 100%;
|
|
|
- height: 100%;
|
|
|
- min-height: 500px;
|
|
|
+ height: 100vh;
|
|
|
+ position: relative;
|
|
|
background: #010813;
|
|
|
- overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.map-container {
|
|
|
@@ -270,159 +300,189 @@ export default {
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
-/* 头部样式:增加了搜索表单布局 */
|
|
|
-.map-header {
|
|
|
- position: absolute;
|
|
|
- top: 55px;
|
|
|
- left: 50px;
|
|
|
- right: 50px;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- z-index: 10;
|
|
|
+/* --- 红绿灯圆点:去掉数字后的纯净样式 --- */
|
|
|
+::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;
|
|
|
+ animation: light-breathe 2s infinite ease-in-out;
|
|
|
}
|
|
|
|
|
|
-/* 搜索表单容器 */
|
|
|
-.search-form {
|
|
|
- display: flex;
|
|
|
- background: rgba(13, 35, 67, 0.9);
|
|
|
- border: 1px solid #1a4a8d;
|
|
|
+::v-deep .pure-light-node:hover {
|
|
|
+ transform: scale(1.4);
|
|
|
+ filter: brightness(1.2);
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes light-breathe {
|
|
|
+
|
|
|
+ 0%,
|
|
|
+ 100% {
|
|
|
+ opacity: 0.7;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ 50% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1.15);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* --- 仿真弹窗样式 --- */
|
|
|
+::v-deep .traffic-window {
|
|
|
+ width: 240px;
|
|
|
+ background: rgba(7, 21, 43, 0.95);
|
|
|
+ border: 1px solid #32c5ff;
|
|
|
border-radius: 4px;
|
|
|
+ color: #fff;
|
|
|
overflow: hidden;
|
|
|
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
|
}
|
|
|
|
|
|
-.search-input {
|
|
|
- background: transparent;
|
|
|
- border: none;
|
|
|
- padding: 10px 15px;
|
|
|
- color: #fff;
|
|
|
- width: 240px;
|
|
|
- outline: none;
|
|
|
+.window-header {
|
|
|
+ padding: 8px 12px;
|
|
|
font-size: 13px;
|
|
|
+ font-weight: bold;
|
|
|
+ clip-path: polygon(0 0, 100% 0, 92% 100%, 0% 100%);
|
|
|
}
|
|
|
|
|
|
-.search-input::placeholder {
|
|
|
- color: #5b7da8;
|
|
|
+.window-body {
|
|
|
+ padding: 15px;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.search-btn {
|
|
|
- background: #1a4a8d;
|
|
|
- border: none;
|
|
|
- color: #fff;
|
|
|
- padding: 0 20px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 13px;
|
|
|
- transition: background 0.2s;
|
|
|
+.data-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 8px;
|
|
|
}
|
|
|
|
|
|
-.search-btn:hover {
|
|
|
- background: #2660b3;
|
|
|
+.label {
|
|
|
+ color: #8da6c7;
|
|
|
}
|
|
|
|
|
|
-.action-box {
|
|
|
- background: rgba(13, 35, 67, 0.9);
|
|
|
- border: 1px solid #1a4a8d;
|
|
|
- padding: 10px 18px;
|
|
|
- color: #fff;
|
|
|
- border-radius: 4px;
|
|
|
- font-size: 13px;
|
|
|
- cursor: pointer;
|
|
|
+.highlight {
|
|
|
+ color: #32c5ff;
|
|
|
+ font-family: 'Digital-7', sans-serif;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-container {
|
|
|
+ height: 3px;
|
|
|
+ background: #1a2b45;
|
|
|
+ margin-top: 10px;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-bar {
|
|
|
+ height: 100%;
|
|
|
+ transition: width 0.3s;
|
|
|
}
|
|
|
|
|
|
-/* 图例与其它样式保持不变 */
|
|
|
.map-legend {
|
|
|
position: absolute;
|
|
|
- right: 50px;
|
|
|
- bottom: 40px;
|
|
|
- width: 170px;
|
|
|
- background: rgba(8, 20, 36, 0.95);
|
|
|
- border: 1px solid rgba(38, 74, 124, 0.8);
|
|
|
- border-radius: 12px;
|
|
|
- padding: 18px;
|
|
|
+ bottom: 30px;
|
|
|
+ right: 40px;
|
|
|
+ width: 140px;
|
|
|
+ background: rgba(5, 22, 45, 0.9);
|
|
|
+ border: 1px solid #1e4d8e;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 6px;
|
|
|
z-index: 100;
|
|
|
- box-shadow: 0 0 25px rgba(0, 0, 0, 0.6);
|
|
|
}
|
|
|
|
|
|
.legend-title {
|
|
|
color: #fff;
|
|
|
- font-size: 18px;
|
|
|
- margin-bottom: 18px;
|
|
|
- font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ border-bottom: 1px solid #1e4d8e;
|
|
|
+ padding-bottom: 8px;
|
|
|
}
|
|
|
|
|
|
.legend-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 14px;
|
|
|
+ margin-bottom: 10px;
|
|
|
cursor: pointer;
|
|
|
- transition: 0.2s;
|
|
|
+ transition: 0.3s;
|
|
|
}
|
|
|
|
|
|
-.legend-item.is-hidden {
|
|
|
+.legend-item.is-inactive {
|
|
|
opacity: 0.2;
|
|
|
filter: grayscale(1);
|
|
|
}
|
|
|
|
|
|
.legend-dot {
|
|
|
- width: 12px;
|
|
|
- height: 12px;
|
|
|
- border-radius: 3px;
|
|
|
- margin-right: 12px;
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ margin-right: 10px;
|
|
|
+ border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
.legend-label {
|
|
|
+ flex: 1;
|
|
|
color: #d0d9e2;
|
|
|
- font-size: 14px;
|
|
|
+ font-size: 13px;
|
|
|
}
|
|
|
|
|
|
-::v-deep .pulse-dot {
|
|
|
- width: 12px;
|
|
|
- height: 12px;
|
|
|
- background: #fff;
|
|
|
- border-radius: 50%;
|
|
|
- box-shadow: 0 0 10px #fff, 0 0 20px rgba(50, 197, 255, 0.5);
|
|
|
+.legend-status {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #5b7da8;
|
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
-<style>
|
|
|
-/* 弹窗样式 */
|
|
|
-.custom-info-card {
|
|
|
- background: rgba(5, 22, 45, 0.98);
|
|
|
- border: 1px solid #1e4d8e;
|
|
|
- border-radius: 6px;
|
|
|
- width: 240px;
|
|
|
- color: #fff;
|
|
|
- overflow: hidden;
|
|
|
+.all-select {
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
+ padding-bottom: 12px;
|
|
|
+ margin-bottom: 12px !important;
|
|
|
}
|
|
|
|
|
|
-.card-header {
|
|
|
- padding: 12px 15px;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: bold;
|
|
|
+.all-select .legend-dot {
|
|
|
+ border-radius: 2px;
|
|
|
+ transition: all 0.3s;
|
|
|
}
|
|
|
|
|
|
-.card-body {
|
|
|
- padding: 15px;
|
|
|
- font-size: 13px;
|
|
|
+/* 搜索框容器 */
|
|
|
+.map-search-bar {
|
|
|
+ position: absolute;
|
|
|
+ top: 55px;
|
|
|
+ left: 50px;
|
|
|
+ z-index: 100;
|
|
|
+ display: flex;
|
|
|
+ background: rgba(7, 21, 43, 0.9);
|
|
|
+ border: 1px solid #1e4d8e;
|
|
|
+ padding: 4px;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
|
|
|
}
|
|
|
|
|
|
-.info-row {
|
|
|
- margin-bottom: 8px;
|
|
|
- display: flex;
|
|
|
+.map-search-bar input {
|
|
|
+ width: 200px;
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+ color: #fff;
|
|
|
+ padding: 8px 12px;
|
|
|
+ outline: none;
|
|
|
+ font-size: 13px;
|
|
|
}
|
|
|
|
|
|
-.info-row .label {
|
|
|
- color: #8da6c7;
|
|
|
- width: 70px;
|
|
|
+.map-search-bar input::placeholder {
|
|
|
+ color: #5b7da8;
|
|
|
}
|
|
|
|
|
|
-.amap-info-content {
|
|
|
- background: transparent !important;
|
|
|
- border: none !important;
|
|
|
- padding: 0 !important;
|
|
|
+.map-search-bar button {
|
|
|
+ background: #1e4d8e;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ padding: 0 15px;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 2px;
|
|
|
+ transition: background 0.3s;
|
|
|
+ font-size: 13px;
|
|
|
}
|
|
|
|
|
|
-.amap-info-sharp {
|
|
|
- display: none !important;
|
|
|
+.map-search-bar button:hover {
|
|
|
+ background: #32c5ff;
|
|
|
}
|
|
|
</style>
|