Forráskód Böngészése

feat: 1、重构地图实现方式 2、使用高德地图的driving接口实现所有路线都是真实路线 3、每个路线模拟红绿灯,红绿灯间距500米 4、红绿灯、路线、图例三者实现关联 5、重新搜索和图例样式 6、路线圆点代表红绿灯,实现与设计图一致的字显示等

sequoia tungfang 1 hónapja%!(EXTRA string=óta)
szülő
commit
e9024efe90
1 módosított fájl, 326 hozzáadás és 266 törlés
  1. 326 266
      src/components/TongzhouTrafficMap.vue

+ 326 - 266
src/components/TongzhouTrafficMap.vue

@@ -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>