|
@@ -0,0 +1,377 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="map-wrapper">
|
|
|
|
|
+ <div ref="mapContainer" class="map-container"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="map-header" v-if="initialized">
|
|
|
|
|
+ <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>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="map-legend" v-if="initialized">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import AMapLoader from '@amap/amap-jsapi-loader';
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: "TrafficMap",
|
|
|
|
|
+ props: {
|
|
|
|
|
+ amapKey: { type: String, default: '您的Key' },
|
|
|
|
|
+ securityJsCode: { type: String, default: '您的安全密钥' }
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ AMap: null,
|
|
|
|
|
+ map: null,
|
|
|
|
|
+ infoWindow: null,
|
|
|
|
|
+ initialized: false,
|
|
|
|
|
+ searchQuery: '', // 搜索内容绑定
|
|
|
|
|
+ activeLegends: [],
|
|
|
|
|
+ legendConfig: [
|
|
|
|
|
+ { 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: {}
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.initAMap();
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ if (this.map) {
|
|
|
|
|
+ this.map.destroy();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ async initAMap() {
|
|
|
|
|
+ window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
|
|
|
|
|
+ try {
|
|
|
|
|
+ const AMap = await AMapLoader.load({
|
|
|
|
|
+ key: this.amapKey,
|
|
|
|
|
+ version: "2.0",
|
|
|
|
|
+ plugins: ['AMap.InfoWindow', 'AMap.Polyline', 'AMap.Marker']
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.AMap = AMap;
|
|
|
|
|
+ this.map = new AMap.Map(this.$refs.mapContainer, {
|
|
|
|
|
+ zoom: 13,
|
|
|
|
|
+ center: [116.6612, 39.9125],
|
|
|
|
|
+ mapStyle: "amap://styles/darkblue",
|
|
|
|
|
+ viewMode: "3D"
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.infoWindow = new AMap.InfoWindow({
|
|
|
|
|
+ isCustom: true,
|
|
|
|
|
+ offset: new AMap.Pixel(0, -15)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.map.on("complete", () => {
|
|
|
|
|
+ this.initialized = true;
|
|
|
|
|
+ this.activeLegends = this.legendConfig.map(l => l.type);
|
|
|
|
|
+ this.drawTrafficScene();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.map.on('click', () => this.infoWindow && this.infoWindow.close());
|
|
|
|
|
+
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("地图加载失败:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 搜索查询逻辑
|
|
|
|
|
+ handleSearch() {
|
|
|
|
|
+ if (!this.searchQuery.trim()) {
|
|
|
|
|
+ alert("请输入查询内容");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log("正在执行搜索:", this.searchQuery);
|
|
|
|
|
+ // 这里可以扩展具体的搜索逻辑,比如搜索 POI 或在已有 overlayGroups 中高亮匹配项
|
|
|
|
|
+ alert(`已提交查询:${this.searchQuery}`);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ 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 = [];
|
|
|
|
|
+
|
|
|
|
|
+ const glow = new this.AMap.Polyline({
|
|
|
|
|
+ path: road.path, strokeColor: config.color, strokeOpacity: 0.15, strokeWeight: 20, map: this.map
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const line = new this.AMap.Polyline({
|
|
|
|
|
+ path: road.path, strokeColor: config.color, strokeWeight: 6, map: this.map
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ [glow, line].forEach(item => {
|
|
|
|
|
+ item.on('click', (e) => this.openDetailWindow(road, e.lnglat));
|
|
|
|
|
+ group.push(item);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+ 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>
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ this.infoWindow.setContent(content);
|
|
|
|
|
+ this.infoWindow.open(this.map, lnglat);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleLegendClick(type) {
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.map-wrapper {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 500px;
|
|
|
|
|
+ background: #010813;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 头部样式:增加了搜索表单布局 */
|
|
|
|
|
+.map-header {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ left: 20px;
|
|
|
|
|
+ right: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 搜索表单容器 */
|
|
|
|
|
+.search-form {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: rgba(13, 35, 67, 0.9);
|
|
|
|
|
+ border: 1px solid #1a4a8d;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-input {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ padding: 10px 15px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ width: 240px;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-input::placeholder {
|
|
|
|
|
+ color: #5b7da8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-btn {
|
|
|
|
|
+ background: #1a4a8d;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-btn:hover {
|
|
|
|
|
+ background: #2660b3;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 图例与其它样式保持不变 */
|
|
|
|
|
+.map-legend {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 25px;
|
|
|
|
|
+ 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;
|
|
|
|
|
+ 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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-item.is-hidden {
|
|
|
|
|
+ opacity: 0.2;
|
|
|
|
|
+ filter: grayscale(1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-dot {
|
|
|
|
|
+ width: 12px;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ margin-right: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-label {
|
|
|
|
|
+ color: #d0d9e2;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+::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);
|
|
|
|
|
+}
|
|
|
|
|
+</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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-header {
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-body {
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-row {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-row .label {
|
|
|
|
|
+ color: #8da6c7;
|
|
|
|
|
+ width: 70px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amap-info-content {
|
|
|
|
|
+ background: transparent !important;
|
|
|
|
|
+ border: none !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amap-info-sharp {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|