Преглед изворни кода

Merge branch 'master' of http://121.40.40.223:3000/zizhong.wang/dtScreen

hebotao пре 1 месец
родитељ
комит
6d930b8838
1 измењених фајлова са 289 додато и 218 уклоњено
  1. 289 218
      src/components/TongzhouTrafficMap.vue

+ 289 - 218
src/components/TongzhouTrafficMap.vue

@@ -2,11 +2,6 @@
   <div class="map-wrapper">
     <div ref="mapContainer" class="map-container"></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" :style="privateStyle.legend">
       <div class="legend-title">图例</div>
       <div class="legend-list">
@@ -16,13 +11,8 @@
           <div class="legend-label" style="font-weight: bold;">全选</div>
         </div>
 
-        <div
-          v-for="item in legendConfig"
-          class="legend-item"
-          @click="toggleRouteVisible(item.name)"
-          :key="item.name"
-          :class="{ 'is-inactive': !activeLegends.includes(item.name) }"
-        >
+        <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>
@@ -50,10 +40,7 @@ export default {
       infoWindow: null,
       routeGroups: {},
       polylines: [],
-      searchKey: '',
-      placeSearch: null,
       privateStyle: {
-        search: {},
         legend: {}
       },
       activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
@@ -78,7 +65,6 @@ export default {
 
     // 自定义首页地图搜索和图例位置样式
     if (this.$route.path === '/home') {
-      this.privateStyle.search = { top: "100px", left: "25%" };
       this.privateStyle.legend = { right: "25%" };
     }
   },
@@ -102,134 +88,134 @@ export default {
         this.AMap = await AMapLoader.load({
           key: this.amapKey,
           version: "2.0",
-          plugins: ['AMap.Driving', 'AMap.GeometryUtil', 'AMap.PlaceSearch'] // 必须有这个
+          plugins: ['AMap.Driving', 'AMap.GeometryUtil']
         });
 
         this.map = new this.AMap.Map(this.$refs.mapContainer, {
-          zoom: 14,
+          zoom: 13.5,
           mapStyle: "amap://styles/darkblue",
           center: [116.663, 39.905],
         });
 
-        // 关键:在这里初始化,确保 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('未找到相关位置');
-          }
-        });
-      }
-    },
-
     drawStaticRoutes() {
       const AMap = this.AMap;
 
-      this.legendConfig.forEach((config) => {
-        const driving = new AMap.Driving({
-          map: null,
-          hideMarkers: true,
-          autoFitView: false
-        });
-
-        driving.search(config.start, config.end, (status, result) => {
-          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 {
-            // --- 情况 B: 规划失败,使用保底策略(直接连接起终点) ---
-            console.warn(`${config.name} 路径规划失败,切换为保底直线模式`);
-            path = [config.start, config.end];
-          }
-
-          // 1. 统一绘制路线(无论是精确路网还是直线)
-          const polyline = new AMap.Polyline({
-            path: path,
-            strokeColor: config.color,
-            strokeWeight: 8,
-            strokeOpacity: 0.8,
-            showDir: false,      // 【修改】去掉白色方向箭头
-            lineJoin: 'round',   // 【新增】使拐角更圆润
-            zIndex: 15,          // 【新增】层级设为 15,高于路况图层
-            map: null
+      this.legendConfig.forEach((config, index) => {
+        setTimeout(() => {
+          const driving = new AMap.Driving({
+            map: null,
+            hideMarkers: true,
+            autoFitView: false
           });
 
-          // 2. 在路径上分布点
-          // 如果只有两个点(直线),就只取起点和终点;如果有路网,取起中终
-          const points = path.length > 2
-            ? [path[0], path[Math.floor(path.length / 2)], path[path.length - 1]]
-            : [path[0], path[1]];
-
-          points.forEach(pos => {
-            markers.push(this.createTrafficLightMarker(pos, config));
+          driving.search(config.start, config.end, (status, result) => {
+            let markers = [];
+            let path = [];
+
+            if (status === 'complete' && result.routes[0]) {
+              const route = result.routes[0];
+              route.steps.forEach(step => { path = path.concat(step.path); });
+            } else {
+              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);
+
+            if (needRouteLine) {
+              polyline = new AMap.Polyline({
+                path: path,
+                strokeColor: config.color,
+                strokeWeight: 8,
+                strokeOpacity: 0.8,
+                showDir: false,
+                lineJoin: 'round',
+                zIndex: 15,
+                map: null
+              });
+            }
+
+
+            // 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 => {
+              markers.push(this.createTrafficLightMarker(pos, config));
+            });
+
+            // 3. 【关键修改】:overlays 数组只存放 markers,不再存放 polyline
+            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);
+            }
           });
-
-          // 3. 捆绑入组
-          const group = new AMap.OverlayGroup([...markers, polyline]);
-          this.map.add(group);
-          this.$set(this.routeGroups, config.name, group);
-
-          // 初始化显隐
-          if (!this.activeLegends.includes(config.name)) group.hide();
-        });
+        }, index * 250);
       });
     },
 
-    // 创建交通灯点
     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 needsFlash = ["离线", "降级", "故障"].includes(config.name);
+      const displayStatus = needsFlash ? config.name : "正常运行";
+      const lng = Number(position[0] || position.lng);
+      const lat = Number(position[1] || position.lat);
 
       const marker = new this.AMap.Marker({
-        position: position,
-        // 去掉了数字显示,仅保留呼吸灯效果
-        content: `<div class="pure-light-node ${['离线', '降级', '故障'].includes(config.name) ? 'breathe' : ''}" style="background: ${config.color}; box-shadow: 0 0 15px ${config.color}; font-size: 12px; display: flex; justify-content: center; align-items: center; color: #fff; padding: 8px;"><span>${config?.name?.charAt(0)}</span></div>`,
+        position: [lng, lat],
+        zIndex: 100,
+        content: `
+          <div class="pure-light-node ${needsFlash ? 'breathe' : ''}" 
+              style="background: ${config.color}; box-shadow: 0 0 15px ${config.color}; font-size: 12px; display: flex; justify-content: center; align-items: center; color: #fff; padding: 8px;">
+            <span>${config.name.charAt(0)}</span>
+          </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()
-          }
+          statusColor: config.color, // 统一弹窗小圆点颜色
+          statusLabel: displayStatus, // 统一弹窗状态文字
+          road: '北京路与南京路',
+          time: '2026.1.23.12:00'
         }
       });
 
@@ -237,59 +223,145 @@ 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="traffic-window">
-          <div class="window-header" style="background: ${data.lightDetail.color}">
-            <span class="title">路口信号机: ${data.name}</span>
+        <div class="custom-info-card">
+          <div class="close-btn" onclick="window.closeMapInfoWindow()">✕</div>
+          
+          <div class="card-header">
+            <div class="status-dot" style="background: ${data.statusColor}">
+              <span>${data.name.charAt(0)}</span>
+            </div>
+            <span class="status-text">${data.statusLabel}</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 class="card-body">
+            <div class="info-line">
+              <span class="label">路口:</span>
+              <span class="value">${data.road}</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 class="info-line">
+              <span class="label">发生时间:</span>
+              <span class="value digital">${data.time}</span>
             </div>
           </div>
         </div>
       `;
 
+      // 定义全局关闭方法(因为 isCustom:true 下 Vue 事件会失效)
+      window.closeMapInfoWindow = () => {
+        if (this.infoWindow) this.infoWindow.close();
+      };
+
       if (!this.infoWindow) {
-        this.infoWindow = new this.AMap.InfoWindow({ isCustom: true, offset: new this.AMap.Pixel(0, -20) });
+        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);
     },
 
     // 全选/全不选逻辑
+    // 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() {
-      if (this.isAllSelected) {
-        // 如果当前是全选,则清空激活列表,并隐藏地图上所有组
+      const targetState = !this.isAllSelected; // 获取点击后的目标状态(true为全选,false为全不选)
+
+      if (targetState) {
+        // --- 情况 1:执行“全选” ---
+        // 1. 更新激活列表
+        this.activeLegends = this.legendConfig.map(item => item.name);
+
+        // 2. 遍历所有已经生成的路线数组,将其全部添加到地图上
+        Object.keys(this.routeGroups).forEach(name => {
+          const overlays = this.routeGroups[name]; // 此时 routeGroups 存储的是数组
+          if (overlays && overlays.length > 0) {
+            this.map.add(overlays);
+          }
+        });
+      } else {
+        // --- 情况 2:执行“全不选” ---
+        // 1. 清空激活列表
         this.activeLegends = [];
-        Object.values(this.routeGroups).forEach(group => group && group.hide());
+
+        // 2. 遍历所有路线数组,从地图上移除
+        Object.keys(this.routeGroups).forEach(name => {
+          const overlays = this.routeGroups[name];
+          if (overlays && overlays.length > 0) {
+            this.map.remove(overlays);
+          }
+        });
+
+        // 3. 关闭当前可能打开的弹窗
         if (this.infoWindow) this.infoWindow.close();
-      } else {
-        // 如果当前不是全选,则填充所有图例名称,并显示地图上所有组
-        this.activeLegends = this.legendConfig.map(item => item.name);
-        Object.values(this.routeGroups).forEach(group => group && group.show());
       }
     },
 
     // 保留你原有的单个切换方法,但确保逻辑一致
+    // 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 group = this.routeGroups[name];
+      const overlays = 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();
+        this.map.remove(overlays); // 改用 remove
+        if (this.infoWindow) this.infoWindow.close();
       } else {
         this.activeLegends.push(name);
-        group && group.show();
+        this.map.add(overlays); // 改用 add
       }
     }
   }
@@ -351,55 +423,97 @@ export default {
   }
 }
 
-/* --- 仿真弹窗样式 --- */
-::v-deep .traffic-window {
-  width: 240px;
-  background: rgba(7, 21, 43, 0.95);
-  border: 1px solid #32c5ff;
-  border-radius: 4px;
+/* 关闭按钮样式 */
+::v-deep .close-btn {
+  position: absolute;
+  top: 10px;
+  right: 12px;
+  color: #8da6c7;
+  cursor: pointer;
+  font-size: 16px;
+  transition: color 0.3s;
+  line-height: 1;
+  z-index: 10;
+}
+
+::v-deep .close-btn:hover {
+  color: #ffffff;
+}
+
+/* 确保容器相对定位,以便按钮定位 */
+::v-deep .custom-info-card {
+  position: relative;
+  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);
+}
+
+/* --- 彻底还原图片的自定义弹窗 --- */
+::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;
-  overflow: hidden;
-  box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
 }
 
-.window-header {
-  padding: 8px 12px;
-  font-size: 13px;
+::v-deep .card-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+::v-deep .status-dot {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-right: 8px;
+  font-size: 11px;
+  color: #000;
+  /* 图标内文字为黑色 */
   font-weight: bold;
-  clip-path: polygon(0 0, 100% 0, 92% 100%, 0% 100%);
 }
 
-.window-body {
-  padding: 15px;
-  font-size: 12px;
+::v-deep .status-dot span {
+  transform: scale(0.75);
 }
 
-.data-row {
-  display: flex;
-  justify-content: space-between;
-  margin-bottom: 8px;
+::v-deep .status-text {
+  font-size: 15px;
+  font-weight: bold;
 }
 
-.label {
-  color: #8da6c7;
+::v-deep .info-line {
+  display: flex;
+  margin-bottom: 6px;
+  font-size: 13px;
+  align-items: center;
 }
 
-.highlight {
-  color: #32c5ff;
-  font-family: 'Digital-7', sans-serif;
-  font-size: 14px;
+::v-deep .label {
+  color: #8da6c7;
+  /* 标签灰色 */
+  white-space: nowrap;
 }
 
-.progress-container {
-  height: 3px;
-  background: #1a2b45;
-  margin-top: 10px;
-  border-radius: 2px;
+::v-deep .value {
+  color: #ffffff;
+  /* 内容白色 */
 }
 
-.progress-bar {
-  height: 100%;
-  transition: width 0.3s;
+::v-deep .digital {
+  font-family: 'Consolas', monospace;
+  /* 模拟数字字体 */
 }
 
 .map-legend {
@@ -475,47 +589,4 @@ export default {
   width: 10px;
   height: 10px;
 }
-
-/* 搜索框容器 */
-.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);
-}
-
-.map-search-bar input {
-  width: 200px;
-  background: transparent;
-  border: none;
-  color: #fff;
-  padding: 8px 12px;
-  outline: none;
-  font-size: 13px;
-}
-
-.map-search-bar input::placeholder {
-  color: #5b7da8;
-}
-
-.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;
-}
-
-.map-search-bar button:hover {
-  background: #32c5ff;
-}
 </style>