Browse Source

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

hebotao 1 month ago
parent
commit
d9e64e1f3d

BIN
src/assets/images/login-background.png


+ 371 - 210
src/components/TongzhouTrafficMap.vue

@@ -2,8 +2,12 @@
   <div class="map-wrapper">
     <div ref="mapContainer" class="map-container"></div>
 
-    <div class="map-legend" :style="privateStyle.legend">
-      <div class="legend-title">图例</div>
+    <div class="map-legend" :style="privateStyle.legend" v-if="(!mode || mode === '路口')"
+      :class="{ 'legend-hidden': !legendVisible }">
+      <div class="legend-header">
+        <div class="legend-title">图例</div>
+        <div class="legend-close-btn" @click="toggleLegend">✕</div>
+      </div>
       <div class="legend-list">
         <div class="legend-item all-select" @click="toggleAll">
           <div class="legend-dot"
@@ -12,7 +16,8 @@
         </div>
 
         <div v-for="item in legendConfig" class="legend-item" @click="toggleRouteVisible(item.name)" :key="item.name"
-          :class="{ 'is-inactive': !activeLegends.includes(item.name) }">
+          :class="{ 'is-inactive': !activeLegends.includes(item.name) }"
+          v-if="!mode || (mode === '路口' && !['干线协调', '勤务路线'].includes(item.name))">
 
           <div class="legend-dot"
             :style="{ backgroundColor: ['离线', '降级', '故障'].includes(item.name) ? 'transparent' : item.color }"
@@ -31,6 +36,9 @@
         </div>
       </div>
     </div>
+    <div class="legend-show-btn" v-if="(!mode || mode === '路口') && !legendVisible" @click="toggleLegend">
+      <div class="legend-show-icon">☰</div>
+    </div>
   </div>
 </template>
 
@@ -41,7 +49,8 @@ export default {
   name: "TrafficMap",
   props: {
     amapKey: { type: String, default: '您的Key' },
-    securityJsCode: { type: String, default: '您的安全密钥' }
+    securityJsCode: { type: String, default: '您的安全密钥' },
+    mode: { type: String, default: '', validator: (value) => ['', '路口', '干线', '特勤'].includes(value) }
   },
   data() {
     return {
@@ -53,205 +62,297 @@ export default {
       privateStyle: {
         legend: {}
       },
+      legendVisible: true,
       activeLegends: ["中心计划", "干线协调", "勤务路线", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"],
+      // 核心修正:增加生命周期标识,防止组件销毁后异步回调继续执行
+      _isDestroyed: false,
       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.885], color: "#FF3938" }  // 潞通大街
+        { 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.885], color: "#FF3938" }
       ]
     };
   },
   mounted() {
+    this._isDestroyed = false; // 重置标识
+    this.updateMapByMode();
     this.initAMap();
 
-    // 自定义首页地图搜索和图例位置样式
     if (this.$route.path === '/home') {
       this.privateStyle.legend = { right: "25%" };
     }
   },
+  watch: {
+    mode: {
+      handler() {
+        this.updateMapByMode();
+        this.updateMapDisplay();
+      },
+      immediate: false
+    }
+  },
   beforeDestroy() {
-    if (this.infoWindow) this.infoWindow.close();
-
-    // 1. 清理普通的 polylines 数组
-    this.polylines.forEach(p => {
-      if (p && typeof p.setMap === 'function') p.setMap(null);
-    });
-
-    // 2. 核心修改:清理 routeGroups
-    Object.values(this.routeGroups).forEach(g => {
-      if (!g) return;
-
-      if (Array.isArray(g)) {
-        // 如果是数组(当前的逻辑),遍历每个成员销毁
-        g.forEach(overlay => {
-          if (overlay && typeof overlay.setMap === 'function') {
-            overlay.setMap(null);
-          }
-        });
-      } else if (typeof g.setMap === 'function') {
-        // 如果是旧版的 OverlayGroup 或单个覆盖物
-        g.setMap(null);
+    // 1. 立即设置销毁状态
+    this._isDestroyed = true;
+
+    // 2. 关闭弹窗
+    if (this.infoWindow) {
+      try {
+        this.infoWindow.close();
+      } catch (e) {
+        console.warn('关闭信息窗口时出错:', e);
       }
-    });
+      this.infoWindow = null;
+    }
+
+    // 3. 清理覆盖物引用
+    if (this.routeGroups) {
+      Object.values(this.routeGroups).forEach(overlays => {
+        if (Array.isArray(overlays)) {
+          overlays.forEach(o => {
+            try {
+              if (o.setMap) o.setMap(null);
+            } catch (e) {
+              console.warn('清理覆盖物时出错:', e);
+            }
+          });
+        }
+      });
+      this.routeGroups = {};
+    }
 
+    // 4. 销毁地图实例并清空引用
     if (this.map) {
-      this.map.destroy();
+      try {
+        this.map.destroy();
+      } catch (e) {
+        console.warn('销毁地图实例时出错:', e);
+      }
+      this.map = null;
     }
+
+    // 5. 清理其他引用
+    this.AMap = null;
   },
   computed: {
-    // 判断是否所有图例都在激活列表中
     isAllSelected() {
       return this.activeLegends.length === this.legendConfig.length;
     }
   },
   methods: {
-    // 修改后的 initAMap
+    // 检查地图环境是否安全可用
+    isMapReady() {
+      return !this._isDestroyed && this.map && typeof this.map.add === 'function';
+    },
+
+    updateMapByMode() {
+      switch (this.mode) {
+        case '路口':
+          this.activeLegends = ["中心计划", "定周期控制", "感应控制", "自适应控制", "手动控制", "特殊控制", "离线", "降级", "故障"];
+          break;
+        case '干线':
+          this.activeLegends = ["干线协调"];
+          break;
+        case '特勤':
+          this.activeLegends = ["勤务路线"];
+          break;
+        default:
+          this.activeLegends = this.legendConfig.map(item => item.name);
+      }
+    },
+
+    updateMapDisplay() {
+      if (this.infoWindow) this.infoWindow.close();
+      if (!this.isMapReady()) return;
+
+      Object.keys(this.routeGroups).forEach(name => {
+        const overlays = this.routeGroups[name];
+        if (overlays && overlays.length > 0) {
+          if (this.activeLegends.includes(name)) {
+            this.map.add(overlays);
+          } else {
+            this.map.remove(overlays);
+          }
+        }
+      });
+    },
+
     async initAMap() {
+      if (this._isDestroyed) return;
+      
       window._AMapSecurityConfig = { securityJsCode: this.securityJsCode };
       try {
-        this.AMap = await AMapLoader.load({
+        const AMap = await AMapLoader.load({
           key: this.amapKey,
           version: "2.0",
-          plugins: ['AMap.Driving', 'AMap.GeometryUtil']
+          plugins: ['AMap.Driving']
         });
 
-        this.map = new this.AMap.Map(this.$refs.mapContainer, {
+        // 异步回来后,首先检查组件是否还在
+        if (this._isDestroyed) return;
+
+        this.AMap = AMap;
+        this.map = new AMap.Map(this.$refs.mapContainer, {
           zoom: 13.5,
           mapStyle: "amap://styles/darkblue",
           center: [116.663, 39.905],
         });
 
-        this.map.on('complete', () => this.drawStaticRoutes());
-      } catch (err) { console.error('地图初始化失败', err); }
+        this.map.on('complete', () => {
+          if (!this._isDestroyed) this.drawStaticRoutes();
+        });
+      } catch (err) {
+        console.error('地图加载失败:', err);
+      }
     },
 
     drawStaticRoutes() {
-      const AMap = this.AMap;
+      if (!this.isMapReady()) return;
 
       this.legendConfig.forEach((config, index) => {
+        // 使用箭头函数保持 this 指向 Vue 实例
         setTimeout(() => {
-          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]) {
-              const route = result.routes[0];
-              route.steps.forEach(step => { path = path.concat(step.path); });
-            } else {
-              path = [config.start, config.end];
-            }
-
-            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
-              });
-            }
-
-            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]);
-            }
+          if (!this.isMapReady()) return;
 
-            points.forEach((pos, idx) => {
-              // 临时逻辑,有真实接口后可以删除
-              const posMap = {
-                8: 'pos1',
-                9: 'pos2',
-                10: 'pos3'
-              };
+          try {
+            const driving = new this.AMap.Driving({
+              map: null,
+              hideMarkers: true,
+              autoFitView: false
+            });
 
-              if (idx === 0 && posMap[index]) {
-                localStorage.setItem(posMap[index], pos);
+            // 这里必须用箭头函数 (status, result) => { ... }
+            driving.search(config.start, config.end, (status, result) => {
+              // 这里的 this 才能访问到 isMapReady
+              if (!this.isMapReady()) return;
+
+              try {
+                let path = [];
+                if (status === 'complete' && result && result.routes && result.routes[0]) {
+                  result.routes[0].steps.forEach(step => { path = path.concat(step.path); });
+                } else {
+                  path = [config.start, config.end];
+                }
+
+                const markers = [];
+                let polyline = null;
+
+                // 路线逻辑
+                if (["干线协调", "勤务路线"].includes(config.name)) {
+                  polyline = new this.AMap.Polyline({
+                    path: path,
+                    strokeColor: config.color,
+                    strokeWeight: 6,
+                    strokeOpacity: 0.6,
+                    zIndex: 15
+                  });
+                }
+
+                // --- 稀疏化逻辑 ---
+                const isAbnormalStatus = ["离线", "降级", "故障"].includes(config.name);
+                // 异常状态(离线等)每 40 个点取一个,普通状态每 15 个点取一个
+                const stepSize = isAbnormalStatus ? 40 : 15;
+
+                for (let i = 0; i < path.length; i += stepSize) {
+                  markers.push(this.createTrafficLightMarker(path[i], config));
+                }
+
+                const overlays = [...markers, polyline].filter(Boolean);
+                this.routeGroups[config.name] = overlays;
+
+                if (this.isMapReady() && this.activeLegends.includes(config.name)) {
+                  this.map.add(overlays);
+                }
+              } catch (e) {
+                console.warn('处理路线数据时出错:', e);
               }
-              // 临时逻辑,有真实接口后可以删除
-
-              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);
-            }
-          });
-        }, index * 250);
+          } catch (e) {
+            console.warn('创建驾车实例时出错:', e);
+          }
+        }, index * 200);
       });
     },
 
     createTrafficLightMarker(position, config) {
-      // 根据业务需求:离线、降级、故障需要闪烁,其他保持静止
-      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: [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>
+      if (!position || !config) return null;
+      
+      try {
+        const isAbnormal = ["离线", "降级", "故障"].includes(config.name);
+        const lng = Number(position[0] || position.lng);
+        const lat = Number(position[1] || position.lat);
+
+        // 验证坐标有效性
+        if (isNaN(lng) || isNaN(lat)) return null;
+
+        // 3. 【视觉优化】调整 Marker 大小比例
+        // 异常状态图标略大(为了警示),普通点位略小且半透明
+        const size = isAbnormal ? '18px' : '14px';
+        const opacity = isAbnormal ? '1' : '0.85';
+        const shadow = isAbnormal ? `0 0 10px ${config.color || '#999'}` : `0 0 5px ${config.color || '#999'}`;
+
+        const marker = new this.AMap.Marker({
+          position: [lng, lat],
+          zIndex: isAbnormal ? 110 : 100, // 异常图标显示在更上层
+          content: `
+          <div class="pure-light-node ${isAbnormal ? 'breathe abnormal-node' : ''}" 
+              style="
+                width: ${size}; 
+                height: ${size}; 
+                background: ${config.color || '#999'}; 
+                box-shadow: ${shadow}; 
+                opacity: ${opacity};
+                border: 1.5px solid rgba(255,255,255,0.7);
+                font-size: 12px;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                color: #fff;
+                padding: 8px;
+              ">
+            <span style="transform: scale(0.7); font-weight: bold;">${config.name.charAt(0)}</span>
           </div>
         `,
-        offset: new this.AMap.Pixel(-10, -10),
+        offset: new this.AMap.Pixel(-8, -8),
         extData: {
           ...config,
           position: [lng, lat],
-          statusColor: config.color, // 统一弹窗小圆点颜色
-          statusLabel: displayStatus, // 统一弹窗状态文字
+          statusColor: config.color || '#999',
+          statusLabel: isAbnormal ? config.name : "正常运行",
           road: '北京路与南京路',
           time: '2026.1.23.12:00'
         }
       });
 
       marker.on('click', (e) => {
-        this.openLightInfo(e.target.getExtData(), e.lnglat);
-        // 抛出地图路口点击事件
-        this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
+        if (!this._isDestroyed) {
+          this.openLightInfo(e.target.getExtData(), e.lnglat);
+          this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
+        }
       });
-      
+
       return marker;
+      } catch (e) {
+        console.warn('创建标记时出错:', e);
+        return null;
+      }
     },
 
     openLightInfo(data, position) {
+      if (!this.isMapReady()) return;
+
+      const infoWindowId = `info-window-${Date.now()}`;
       const content = `
-        <div class="custom-info-card">
-          <div class="close-btn" onclick="window.closeMapInfoWindow()">✕</div>
-          
+        <div class="custom-info-card" id="${infoWindowId}">
+          <div class="close-btn" data-id="${infoWindowId}">✕</div>
           <div class="card-header">
             <div class="status-dot" style="background: ${data.statusColor}">
               <span>${data.name.charAt(0)}</span>
@@ -259,109 +360,90 @@ export default {
             <span class="status-text">${data.statusLabel}</span>
           </div>
           <div class="card-body">
-            <div class="info-line">
-              <span class="label">路口:</span>
-              <span class="value">${data.road}</span>
-            </div>
-            <div class="info-line">
-              <span class="label">发生时间:</span>
-              <span class="value digital">${data.time}</span>
-            </div>
+            <div class="info-line"><span class="label">路口:</span><span class="value">${data.road}</span></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.setContent(content);
       this.infoWindow.open(this.map, position);
+      
+      // 添加关闭按钮事件监听器
+      setTimeout(() => {
+        const closeBtn = document.querySelector(`#${infoWindowId} .close-btn`);
+        if (closeBtn) {
+          closeBtn.addEventListener('click', () => {
+            if (this.infoWindow) this.infoWindow.close();
+          });
+        }
+      }, 100);
     },
 
-    // 全选/全不选逻辑修正版
     toggleAll() {
-      const targetState = !this.isAllSelected; // 获取点击后的目标状态(true为全选,false为全不选)
+      const targetState = !this.isAllSelected;
+      if (!this.isMapReady()) return;
 
       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);
-          }
+        Object.values(this.routeGroups).forEach(overlays => {
+          if (overlays && overlays.length > 0) this.map.add(overlays);
         });
       } else {
-        // --- 情况 2:执行“全不选” ---
-        // 1. 清空激活列表
         this.activeLegends = [];
-
-        // 2. 遍历所有路线数组,从地图上移除
-        Object.keys(this.routeGroups).forEach(name => {
-          const overlays = this.routeGroups[name];
-          if (overlays && overlays.length > 0) {
-            this.map.remove(overlays);
-          }
+        Object.values(this.routeGroups).forEach(overlays => {
+          if (overlays && overlays.length > 0) this.map.remove(overlays);
         });
-
-        // 3. 关闭当前可能打开的弹窗
         if (this.infoWindow) this.infoWindow.close();
       }
     },
 
     toggleRouteVisible(name) {
-      const overlays = this.routeGroups[name] || []; // 获取的是数组
+      if (!this.isMapReady()) return;
+
+      const overlays = this.routeGroups[name] || [];
       const index = this.activeLegends.indexOf(name);
       if (index > -1) {
         this.activeLegends.splice(index, 1);
-        this.map.remove(overlays); // 改用 remove
+        this.map.remove(overlays);
         if (this.infoWindow) this.infoWindow.close();
       } else {
         this.activeLegends.push(name);
-        this.map.add(overlays); // 改用 add
+        this.map.add(overlays);
       }
     },
 
-    // 其他组件点击定位到地图指定的点
     focusByLocation(targetPos) {
-      if (!targetPos || targetPos.length !== 2) return;
+      if (!this.isMapReady() || !targetPos || targetPos.length !== 2) return;
 
       let foundMarker = null;
-
-      // 1. 遍历所有路线组
       Object.values(this.routeGroups).forEach(group => {
-        // 2. 在组内寻找 Marker
         const marker = group.find(item => {
           if (!(item instanceof this.AMap.Marker)) return false;
           const pos = item.getExtData().position;
-          // 3. 坐标比对(考虑到浮点数精度,建议使用 AMap 自带的几何工具或简单比对)
-          return pos[0] === targetPos[0] && pos[1] === targetPos[1];
+          return Math.abs(pos[0] - targetPos[0]) < 0.0001 && Math.abs(pos[1] - targetPos[1]) < 0.0001;
         });
         if (marker) foundMarker = marker;
       });
 
       if (foundMarker) {
-        // 4. 定位并打开弹窗
         const finalPos = foundMarker.getPosition();
-        this.map.setZoomAndCenter(17, finalPos, false, 500); // 17级视角,平滑移动
-
+        this.map.setZoomAndCenter(17, finalPos, false, 500);
         setTimeout(() => {
-          this.openLightInfo(foundMarker.getExtData(), finalPos);
+          if (!this._isDestroyed) this.openLightInfo(foundMarker.getExtData(), finalPos);
         }, 600);
-      } else {
-        console.warn("未在地图上找到该坐标对应的点位:", targetPos);
       }
+    },
+
+    toggleLegend() {
+      this.legendVisible = !this.legendVisible;
     }
   }
 };
@@ -380,14 +462,7 @@ export default {
   height: 100%;
 }
 
-::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;
-}
+
 
 ::v-deep .pure-light-node.breathe {
   animation: light-breathe 2s infinite ease-in-out;
@@ -404,19 +479,7 @@ export default {
   filter: brightness(1.2);
 }
 
-@keyframes light-breathe {
-
-  0%,
-  100% {
-    opacity: 0.7;
-    transform: scale(1);
-  }
 
-  50% {
-    opacity: 1;
-    transform: scale(1.15);
-  }
-}
 
 ::v-deep .close-btn {
   position: absolute;
@@ -502,16 +565,76 @@ export default {
   padding: 15px;
   border-radius: 6px;
   z-index: 100;
+  transition: all 0.3s ease-in-out;
+  opacity: 1;
+  max-width: 200px;
+  overflow: hidden;
 }
 
-.legend-title {
-  color: #fff;
-  font-size: 14px;
+.map-legend.legend-hidden {
+  opacity: 0;
+  transform: translateX(calc(100% + 20px));
+  pointer-events: none;
+}
+
+.legend-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
   margin-bottom: 12px;
   border-bottom: 1px solid #1e4d8e;
   padding-bottom: 8px;
 }
 
+.legend-close-btn {
+  color: #8da6c7;
+  cursor: pointer;
+  font-size: 16px;
+  transition: color 0.3s;
+  line-height: 1;
+}
+
+.legend-close-btn:hover {
+  color: #ffffff;
+}
+
+.legend-show-btn {
+  position: absolute;
+  bottom: 30px;
+  right: 20px;
+  background: rgba(5, 22, 45, 0.9);
+  border: 1px solid #1e4d8e;
+  width: 40px;
+  height: 40px;
+  border-radius: 6px 0 0 6px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  z-index: 99;
+  transition: all 0.3s ease-in-out;
+}
+
+.legend-show-btn:hover {
+  background: rgba(10, 30, 60, 0.9);
+  border-color: #3a75c4;
+}
+
+.legend-show-icon {
+  color: #8da6c7;
+  font-size: 18px;
+  transition: color 0.3s;
+}
+
+.legend-show-btn:hover .legend-show-icon {
+  color: #ffffff;
+}
+
+.legend-title {
+  color: #fff;
+  font-size: 14px;
+}
+
 .legend-item {
   display: flex;
   align-items: center;
@@ -630,4 +753,42 @@ export default {
   object-fit: contain;
   display: block;
 }
+
+::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;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  pointer-events: auto;
+  /* 确保能点击 */
+}
+
+/* 异常状态增加稍微剧烈一点的呼吸感,但缩小范围 */
+@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);
+}
 </style>

+ 3 - 3
src/components/ui/IntersectionControlCard.vue

@@ -7,7 +7,7 @@
 
         <div class="card-body">
             <div class="micro-map-container">
-                <IntersectionMapVideos v-if="data.mapData" :mapData="data.mapData" :videoUrls="data.videoUrls || {}" />
+                <IntersectionMap v-if="data.mapData" :mapData="data.mapData" />
             </div>
 
             <div class="info-panel">
@@ -37,11 +37,11 @@
 </template>
 
 <script>
-import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
+import IntersectionMap from '@/components/ui/IntersectionMap.vue';
 
 export default {
     name: 'IntersectionControlCard',
-    components: { IntersectionMapVideos },
+    components: { IntersectionMap },
     props: { 
         data: { type: Object, required: true } 
     },

+ 1 - 1
src/components/ui/OnlineStatusTabs.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="online-status-tabs">
-    <TechTabs v-model="activeTab" :interval="10000" type="segmented"  @tab-click="handleTabClick">
+    <TechTabs v-model="activeTab" :interval="10000" type="segmented" autoPlay  @tab-click="handleTabClick">
       <TechTabPane label="信号机" name="signalMachine">
         <DynamicDonutChart 
           v-if="activeTab === 'signalMachine' && displayData"

+ 89 - 14
src/components/ui/TrafficTimeSpace.vue

@@ -9,7 +9,6 @@ import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
 
 export default {
   name: 'TrafficTimeSpace',
-  // 2. 注册混入,自动接管组件的图表缩放、数据重绘和销毁生命周期
   mixins: [echartsResize], 
   props: {
     intersections: { type: Array, required: true },
@@ -21,17 +20,21 @@ export default {
     scrollSpeed: { type: Number, default: 0.5 },
     upWaveColor: { type: String, default: 'rgba(46, 196, 182, 0.45)' },
     downWaveColor: { type: String, default: 'rgba(104, 231, 95, 0.4)' },
-    waveLabelColor: { type: String, default: '#e0f7fa' }
+    waveLabelColor: { type: String, default: '#e0f7fa' },
+    // 【新增】扫描线颜色配置
+    timeLineColor: { type: String, default: '#FF9800' }
   },
   data() {
     return {
       scrollTimer: null,
-      currentViewTime: 0
-      // 删除了 chart 实例,交由 mixin 的 $_chart 管理
+      currentViewTime: 0,
+      // 【新增】控制扫描线的状态
+      timeLineTimer: null,
+      currentTimeIndicator: 0 
     };
   },
   computed: {
-    maxDistance() { return Math.max(...this.distances); },
+    maxDistance() { return Math.max(...this.distances, 100); }, // 加个兜底值防无数据报错
     maxDataTime() {
       let max = 0;
       this.waveData.forEach(w => max = Math.max(max, w.xBR, w.xTR));
@@ -49,11 +52,14 @@ export default {
     this.$nextTick(() => {
       this.initChart();
       if (this.autoScroll) this.startScroll();
+      // 【新增】组件挂载后启动扫描线
+      this.startTimeLine();
     });
   },
   beforeDestroy() {
     this.stopScroll();
-    // 所有的 resize 和 chart.dispose 统统删掉,mixin 会帮你兜底清理!
+    // 【新增】销毁时清理定时器
+    this.stopTimeLine(); 
   },
   watch: {
     waveData() { this.updateChart(); },
@@ -62,12 +68,10 @@ export default {
   },
   methods: {
     initChart() {
-      // 3. 将实例赋值给 $_chart (mixin 规定的变量)
       this.$_chart = echarts.init(this.$refs.chartContainer);
       this.updateChart();
     },
 
-    // mixin 中的 resize 会在窗口变化时自动调用这个方法
     updateChart() {
       if (!this.$_chart) return;
 
@@ -81,7 +85,6 @@ export default {
         animation: false,
         tooltip: { show: false },
         grid: { 
-          // 4. 网格全部使用 px2echarts
           left: px2echarts(80), 
           right: px2echarts(15), 
           top: px2echarts(10), 
@@ -94,7 +97,7 @@ export default {
           axisLabel: { 
             color: '#7b95b9', 
             formatter: '{value}s', 
-            fontSize: px2echarts(10) // 5. 坐标轴字体
+            fontSize: px2echarts(10)
           },
           splitLine: { show: true, lineStyle: { color: '#1a305d', type: 'solid' } },
           axisLine: { lineStyle: { color: '#31548e' } }
@@ -108,13 +111,15 @@ export default {
             interval: 0,
             color: '#9cb1d4',
             fontWeight: 'bold',
-            fontSize: px2echarts(10), // 5. 坐标轴字体
+            fontSize: px2echarts(10),
             formatter: value => distances.includes(value) ? intersections[distances.indexOf(value)] : ''
           },
           splitLine: { show: true, lineStyle: { color: '#1a305d' } }
         },
+        // 【修改】给每个系列加上 id,新增时间线系列
         series: [
           {
+            id: 'waveSeries',
             type: 'custom',
             renderItem: function (params, api) { return self.renderWave(params, api); },
             data: this.echartsWaveData,
@@ -122,6 +127,7 @@ export default {
             z: 1
           },
           {
+            id: 'redSeries',
             type: 'custom',
             renderItem: function (params, api) { return self.renderRedBackground(params, api); },
             data: this.echartsRedData,
@@ -129,21 +135,29 @@ export default {
             z: 2
           },
           {
+            id: 'greenSeries',
             type: 'custom',
             renderItem: function (params, api) { return self.renderGreenLight(params, api); },
             data: this.echartsGreenData,
             clip: true,
             z: 3
+          },
+          {
+            id: 'timeLineSeries', // 【新增】垂直扫描线系列
+            type: 'custom',
+            renderItem: function (params, api) { return self.renderTimeLine(params, api); },
+            data: [[this.currentTimeIndicator]], // 初始数据
+            clip: true,
+            z: 10 // 放在最上层
           }
         ]
       });
     },
 
-    // 6. renderItem 里的所有死像素全换成了 px2echarts
     renderRedBackground(params, api) {
       const y = api.value(0);
       const startX = api.coord([0, y])[0];
-      const endX = api.coord([this.maxDataTime, y])[0];
+      const endX = api.coord([this.maxDataTime || this.viewWindow, y])[0];
       return {
         type: 'rect',
         shape: { 
@@ -209,6 +223,68 @@ export default {
       };
     },
 
+    // 【新增】渲染垂直扫描线
+    renderTimeLine(params, api) {
+      const xVal = api.value(0);
+      const start = api.coord([xVal, 0]); // 底部坐标
+      const end = api.coord([xVal, this.maxDistance]); // 顶部坐标
+      
+      return {
+        type: 'line',
+        shape: {
+          x1: start[0],
+          y1: start[1],
+          x2: end[0],
+          y2: end[1]
+        },
+        style: {
+          stroke: this.timeLineColor, // 线条颜色
+          lineWidth: px2echarts(2), // 线条粗细
+          lineDash: [px2echarts(5), px2echarts(5)] // 设置为虚线,如果想实线可以删掉这行
+        }
+      };
+    },
+
+    // 【新增】启动扫描线动画
+    startTimeLine() {
+      this.stopTimeLine();
+      let lastTime = Date.now();
+      
+      this.timeLineTimer = setInterval(() => {
+        const now = Date.now();
+        // 计算两次执行的时间差,转换为秒 (真实时间的 1秒 等于 X轴的 1个单位)
+        const delta = (now - lastTime) / 1000; 
+        lastTime = now;
+
+        this.currentTimeIndicator += delta;
+        
+        // 判定周期(当横坐标到达最大数据时间或者当前视窗的最大值时,回到原点)
+        const cycleLimit = this.maxDataTime > 0 ? this.maxDataTime : this.viewWindow;
+
+        if (this.currentTimeIndicator > cycleLimit) {
+          this.currentTimeIndicator = 0;
+        }
+
+        if (this.$_chart) {
+          // 精准更新:只传入对应 id 的系列数据,避免整个图表重绘引发卡顿
+          this.$_chart.setOption({
+            series: [{
+              id: 'timeLineSeries',
+              data: [[this.currentTimeIndicator]]
+            }]
+          });
+        }
+      }, 16); // ~60fps 的刷新率,保证视觉极其平滑
+    },
+
+    // 【新增】停止扫描线动画
+    stopTimeLine() {
+      if (this.timeLineTimer) {
+        clearInterval(this.timeLineTimer);
+        this.timeLineTimer = null;
+      }
+    },
+
     startScroll() {
       this.stopScroll();
       this.scrollTimer = setInterval(() => {
@@ -217,7 +293,6 @@ export default {
           this.currentViewTime = 0;
         }
         if (this.$_chart) {
-          // 注意这里也要换成 $_chart
           this.$_chart.setOption({
             xAxis: { min: this.currentViewTime, max: this.currentViewTime + this.viewWindow }
           });

+ 6 - 7
src/layouts/DashboardLayout.vue

@@ -1,10 +1,10 @@
 <template>
     <div class="fluid-dashboard">
         <div class="frame-top">
-            <div class="title">{{ title }}</div>
             <div class="top-logo">
                 <img src="@/assets/images/logo.png" />
             </div>
+            <div class="title neon-pulse-style">{{ title }}</div>
             <div class="top-right-user">
                 <UserProfile />
             </div>
@@ -149,7 +149,7 @@ export default {
     },
     data() {
         return {
-            title: '交通信号控制平台',
+            title: '灵智交通信号控制平台',
         }
     },
     methods: {
@@ -250,10 +250,7 @@ export default {
 }
 
 /* 恢复具体功能区域的鼠标交互 */
-.top-header,
-.left-sidebar,
-.center-area,
-.right-sidebar {
+.top-header {
     pointer-events: auto;
 }
 
@@ -306,6 +303,8 @@ export default {
 }
 
 /* --- 恢复中心区域内部具体组件(如 BottomDock 和 slot 里的内容)的交互 --- */
+.left-sidebar > *,
+.right-sidebar > *,
 .center-area > * {
     pointer-events: auto; 
 }
@@ -320,7 +319,7 @@ export default {
 .top-logo {
     position: absolute;
     top: 10px;
-    right: 300px;
+    left: 100px;
     height: 30px;
 }
 .top-logo img {

+ 147 - 0
src/layouts/LoginLayout.vue

@@ -0,0 +1,147 @@
+<template>
+    <div class="fluid-dashboard">
+        <div class="frame-top">
+            <div class="title neon-pulse-style">{{ title }}</div>
+        </div>
+
+        <div class="frame-bottom"></div>
+
+        <slot name="background"></slot>
+
+        <div class="ui-layer">
+
+            <main class="main-layout" :class="layoutClass">
+                <slot name="main"></slot>
+            </main>
+        </div>
+
+    </div>
+</template>
+
+<script>
+
+
+export default {
+    name: 'LoginLayout',
+    mixins: [],
+    components: {
+        
+    },
+    props: {
+        // 接收外部传入的 class,用于动态切换 CSS 网格布局
+        // 例如传入 "special-situation-monitoring"
+        layoutClass: {
+            type: String,
+            default: ''
+        }
+    },
+    data() {
+        return {
+            title: '灵智交通信号控制平台',
+        }
+    },
+    methods: {
+        
+    }
+}
+</script>
+
+<style scoped>
+/* ================= 根容器 ================= */
+.fluid-dashboard {
+    width: 100vw;
+    height: 100vh;
+    position: relative;
+    overflow: hidden;
+    background: #050a17;
+    /* 兜底深色背景 */
+}
+
+/* ================= 大屏装饰边框 ================= */
+.frame-top,
+.frame-left,
+.frame-right,
+.frame-bottom {
+    position: absolute;
+    pointer-events: none;
+    /* 【核心】鼠标事件穿透,不挡底层交互 */
+    z-index: 50;
+    /* 层级高于 UI,低于弹窗 */
+}
+
+.frame-top {
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100px;
+    background: url('@/assets/images/layout-top.png') no-repeat center top;
+    background-size: 100% 100%;
+}
+
+.frame-top .title {
+    font-family: var(--title-font-family);
+    font-size: 48px;
+    color: #707070;
+    line-height: 63px;
+    text-align: center;
+    font-style: normal;
+    text-transform: none;
+    /* background: linear-gradient(90deg, #9ED3FD 0%, #FFFFFF 100%); */
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+}
+
+
+.frame-left {
+    top: 10px;
+    left: 0;
+    width: 16px;
+    height: calc(100% - 10px - 40px);
+    background: url('@/assets/images/layout-left.png') no-repeat left center;
+    background-size: 100% 100%;
+}
+
+.frame-right {
+    top: 10px;
+    right: 0;
+    width: 16px;
+    height: calc(100% - 10px - 40px);
+    background: url('@/assets/images/layout-right.png') no-repeat right center;
+    background-size: 100% 100%;
+}
+
+.frame-bottom {
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 17px;
+    /* background: url('@/assets/images/layout-bottom.png') no-repeat center bottom; */ 
+    background-size: 100% 100%;
+}
+
+/* ================= UI 层与网格布局 ================= */
+.ui-layer {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 2;
+    pointer-events: none;
+    /* 让鼠标穿透点到下层的地图 */
+    display: grid;
+    grid-template-rows: 80px 1fr;
+    /* 头部 80px,其余给主体 */
+}
+
+/* --- 主体网格 --- */
+.main-layout {
+    height: 100%;
+    box-sizing: border-box;
+    pointer-events: none;
+}
+
+.main-layout > * {
+    pointer-events: auto; 
+}
+</style>

+ 44 - 0
src/styles/base.css

@@ -135,4 +135,48 @@ html, body {
 
 ::-webkit-scrollbar-corner {
   background: transparent; 
+}
+
+.neon-pulse-style {
+
+    letter-spacing: 4px; /* 增加字间距 */
+    
+    /* 2. 文字渐变:模拟霓虹灯管的亮芯 */
+    background: linear-gradient(
+        to bottom,
+        #ffffff 20%,   /* 核心最亮处 */
+        #b3e5fc 50%,   /* 浅蓝色过渡 */
+        #4fc3f7 100%   /* 底部稍深 */
+    );
+
+    /* 3. 核心:霓虹灯发光效果 (多层 drop-shadow) */
+    /* 第一层:近距离白色高亮 */
+    /* 第二层:中距离主题色发光 */
+    /* 第三层:远距离环境光晕 */
+    filter: 
+        drop-shadow(0 0 5px rgba(255, 255, 255, 0.8))
+        drop-shadow(0 0 15px rgba(79, 195, 247, 0.6))
+        drop-shadow(0 0 30px rgba(3, 169, 244, 0.4));
+
+    /* 4. 动画:让霓虹灯“活”起来 */
+    animation: neon-pulse 3s ease-in-out infinite;
+    transition: all 0.3s ease;
+}
+
+/* 霓虹灯呼吸动画:模拟电流不稳定的真实感 */
+@keyframes neon-pulse {
+    0%, 100% {
+        filter: 
+            drop-shadow(0 0 5px rgba(255, 255, 255, 0.8))
+            drop-shadow(0 0 15px rgba(79, 195, 247, 0.6))
+            drop-shadow(0 0 30px rgba(3, 169, 244, 0.4));
+        opacity: 1;
+    }
+    50% {
+        filter: 
+            drop-shadow(0 0 8px rgba(255, 255, 255, 0.9))
+            drop-shadow(0 0 22px rgba(79, 195, 247, 0.8))
+            drop-shadow(0 0 45px rgba(3, 169, 244, 0.6));
+        opacity: 0.95; /* 微微闪烁 */
+    }
 }

+ 21 - 0
src/views/DataAnalysis.vue

@@ -14,6 +14,8 @@
             <TongzhouTrafficMap
             amapKey="db2da7e3e248c3b2077d53fc809be63f"
             securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
+            :mode="activeLeftTab === 'crossing' ? '路口' : activeLeftTab === 'trunkLine' ? '干线' : activeLeftTab === 'specialDuty' ? '特勤' : ''"
+            @map-crossing-click="handleMapCrossingClick"
             />
         </template>
 
@@ -215,6 +217,25 @@ export default {
             // 这里可以根据 nodeData 的经纬度来控制地图组件的视角
             this.testOpenSecurityRoute(nodeData);
         },
+        // 处理地图点击事件
+        handleMapCrossingClick(mapData, lnglat) {
+            console.log('父组件接收到了地图路口点击事件:', mapData, lnglat);
+            // 组装模拟数据
+            let nodeData = {
+                id: Math.random(1, 100),
+                label: mapData.road,
+            }
+            console.log(nodeData);
+            if (this.activeLeftTab === 'overview') { // 总览
+                this.testOpenSecurityRoute(nodeData);
+            } else if (this.activeLeftTab === 'crossing') { // 路口
+                this.testOpenSecurityRoute(nodeData);
+            } else if (this.activeLeftTab === 'trunkLine') { // 干线
+                this.testOpenSecurityRoute(nodeData);
+            } else if (this.activeLeftTab === 'specialDuty') { // 特勤
+                this.testOpenSecurityRoute(nodeData);
+            }
+        },
         // ================= 测试用例:模拟各种点击行为 =================
 
         // 模拟 1:打开特勤安保路线面板

+ 13 - 10
src/views/Home.vue

@@ -2,7 +2,7 @@
   <DashboardLayout>
     <!-- 天气 -->
     <template #header-left>
-      <WeatherWidget />
+      
     </template>
     <!-- 日期 -->
     <template #header-right>
@@ -11,8 +11,11 @@
 
     <!-- 地图 -->
     <template #map>
-      <TongzhouTrafficMap ref="trafficMapRef" amapKey="db2da7e3e248c3b2077d53fc809be63f"
-        securityJsCode="a7413c674852c5eaf01d90813c5b7ef6" />
+      <TongzhouTrafficMap
+        ref="trafficMapRef"
+        amapKey="db2da7e3e248c3b2077d53fc809be63f"
+        securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
+      />
     </template>
 
     <template #left>
@@ -26,7 +29,7 @@
         </div>
         <div class="panel-item">
           <PanelContainer title="控制模式">
-            <TickDonutChart :chartData="controlInfoData" centerTitle="650个" centerSubTitle="控制信息" />
+            <TickDonutChart :chartData="controlInfoData" centerTitle="666个" centerSubTitle="控制信息" />
           </PanelContainer>
         </div>
         <div class="panel-item">
@@ -146,7 +149,7 @@ export default {
       alarmData: [
         {
           id: '1',
-          title: '通讯中断',
+          title: '1.通讯中断',
           type: 'error', // 渲染为红色
           time: '16:28:28',
           description: '中关村大街-科学院南路口-设备离线',
@@ -181,11 +184,11 @@ export default {
 
       // 2. 模拟数据源
       tableData: [
-        { id: 1, name: '大型活动交通安保', executor: '测试', level: '一级', status: '未开始' },
-        { id: 2, name: '道路施工路段交通引导张飞', executor: '张飞', level: '一级', status: '未开始' },
-        { id: 3, name: '酒驾醉驾专项查缉', executor: '关将', level: '二级', status: '进行中' },
-        { id: 4, name: '交通信号灯故障排查', executor: '刘备', level: '一级', status: '未开始' },
-        { id: 5, name: '应急救援通道清障', executor: '孙权', level: '一级', status: '未开始' }
+        { id: 1, name: '大型活动交通安保', executor: '张明', level: '一级', status: '未开始' },
+        { id: 2, name: '道路施工路段交通引导', executor: '李强', level: '一级', status: '未开始' },
+        { id: 3, name: '酒驾醉驾专项查缉', executor: '王芳', level: '二级', status: '进行中' },
+        { id: 4, name: '交通信号灯故障排查', executor: '赵伟', level: '一级', status: '未开始' },
+        { id: 5, name: '应急救援通道清障', executor: '陈静', level: '一级', status: '未开始' }
       ],
       // 1. 表头
       keyIntersectionColumns: [

+ 93 - 131
src/views/Login.vue

@@ -1,79 +1,86 @@
 <template>
-  <div class="page">
-    <div class="bg"></div>
-    <div class="ai-cloud-ring" aria-hidden="true"></div>
-    <div class="split-door" :class="{ opening: isDoorOpening }" aria-hidden="true">
-      <div class="door left"></div>
-      <div class="door right" @animationend="handleDoorEnd"></div>
-    </div>
-    <div class="header-deco"></div>
-    <!-- <div class="top-title">智慧交通信号控制管理平台</div> -->
-    <div class="content">
-      <!-- 左侧酷炫区域 -->
-      <div class="left">
-        <div class="ellipse-area">
-          <img class="ellipse" :src="require('@/assets/ellipse-line.png')" alt="ellipse" />
-          <!-- 沿椭圆的流光:不旋转椭圆图,只做覆盖层沿线跑动 -->
-          <div class="ellipse-glow"></div>
-          <div class="icon icon-1"><img :src="require('@/assets/icon-upload.png')" width="87px" height="87px" /></div>
-          <div class="icon icon-2"><img :src="require('@/assets/icon-webcam.png')" width="87px" height="87px" /></div>
-          <div class="icon icon-3"><img :src="require('@/assets/icon-shield.png')" width="87px" height="87px" /></div>
-          <div class="icon icon-4"><img :src="require('@/assets/icon-setting.png')" width="87px" height="87px" /></div>
+  <LoginLayout >
+
+    <!-- 背景 -->
+    <template #background>
+      <div class="login-bg"></div>
+    </template>
+
+    <template #main>
+      <div class="page">
+        <div class="ai-cloud-ring" aria-hidden="true"></div>
+        <div class="split-door" :class="{ opening: isDoorOpening }" aria-hidden="true">
+          <div class="door left"></div>
+          <div class="door right" @animationend="handleDoorEnd"></div>
         </div>
-      </div>
-
-      <!-- 右侧登录面板 -->
-      <div class="right">
-        <div class="panel">
-            <div class="panel-inner">
-            
-            <div class="panel-title">账号登录</div>
-
-            <div class="field">
-                <img class="i" :src="require('@/assets/i_user.png')" />
-                <span class="field-label">账号</span>
-                <input class="inp" v-model.trim="username" placeholder="请输入账号" />
-            </div>
-
-            <div class="field">
-                <img class="i" :src="require('@/assets/i_lock.png')" />
-                <span class="field-label">密码</span>
-                <input class="inp" type="password" v-model.trim="password" placeholder="请输入密码" />
+        <div class="content">
+          <!-- 左侧酷炫区域 -->
+          <div class="left">
+            <div class="ellipse-area">
+              <img class="ellipse" :src="require('@/assets/ellipse-line.png')" alt="ellipse" />
+              <!-- 沿椭圆的流光:不旋转椭圆图,只做覆盖层沿线跑动 -->
+              <div class="ellipse-glow"></div>
+              <div class="icon icon-1"><img :src="require('@/assets/icon-upload.png')" width="87px" height="87px" /></div>
+              <div class="icon icon-2"><img :src="require('@/assets/icon-webcam.png')" width="87px" height="87px" /></div>
+              <div class="icon icon-3"><img :src="require('@/assets/icon-shield.png')" width="87px" height="87px" /></div>
+              <div class="icon icon-4"><img :src="require('@/assets/icon-setting.png')" width="87px" height="87px" /></div>
             </div>
-
-            <div class="row">
-                <div class="field cap-field">
-                <img class="i" :src="require('@/assets/i_captcha.png')" />
-                <span class="field-label">验证码</span>
-                <input class="inp" v-model.trim="captchaInput" placeholder="请输入验证码" />
+          </div>
+    
+          <!-- 右侧登录面板 -->
+          <div class="right">
+            <div class="panel">
+                <div class="panel-inner">
+                
+                <div class="panel-title">账号登录</div>
+    
+                <div class="field">
+                    <img class="i" :src="require('@/assets/i_user.png')" />
+                    <span class="field-label">账号</span>
+                    <input class="inp" v-model.trim="username" placeholder="请输入账号" />
                 </div>
-                <CaptchaCanvas v-model="captchaCode" />
-            </div>
-
-            <button class="btn" @click="onLogin">立即登录</button>
-
-            <div class="hint" v-if="hint">{{ hint }}</div>
-        
-            </div>
-         </div>
+    
+                <div class="field">
+                    <img class="i" :src="require('@/assets/i_lock.png')" />
+                    <span class="field-label">密码</span>
+                    <input class="inp" type="password" v-model.trim="password" placeholder="请输入密码" />
+                </div>
+    
+                <div class="row">
+                    <div class="field cap-field">
+                    <img class="i" :src="require('@/assets/i_captcha.png')" />
+                    <span class="field-label">验证码</span>
+                    <input class="inp" v-model.trim="captchaInput" placeholder="请输入验证码" />
+                    </div>
+                    <CaptchaCanvas v-model="captchaCode" />
+                </div>
+                
+                <div class="hint" v-if="hint">{{ hint }}</div>
+                <button class="btn" @click="onLogin">立即登录</button>
+    
+                </div>
+             </div>
+          </div>
+        </div>
+        <div class="page-dim" :class="{ opening: isDoorOpening }" aria-hidden="true"></div>
+        <div class="copyright">
+          <img class="copyright-logo" :src="require('@/assets/images/logo.png')" />
+          <div>北京东土正创科技有限公司</div>
+        </div>
       </div>
-    </div>
-    <div class="page-dim" :class="{ opening: isDoorOpening }" aria-hidden="true"></div>
-    <div class="copyright">
-      <img class="copyright-logo" :src="require('@/assets/images/logo.png')" />
-    </div>
-  </div>
+    </template>
+
+  </LoginLayout>
 </template>
 
 <script>
 import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
+import LoginLayout from "@/layouts/LoginLayout.vue";
 import { mockLogin } from "@/mock/api";
-import Toast from "@/plugins/toast";
 
 export default {
-  // eslint-disable-next-line vue/multi-word-component-names
-  name: "Login",
-  components: { CaptchaCanvas },
+  name: "LoginPage",
+  components: { CaptchaCanvas, LoginLayout },
   created() {
     // 提前预加载 Cesium 瓦片
     import('@/utils/cesiumPreloader').then(m => m.default.start());
@@ -118,14 +125,11 @@ export default {
       });
 
       if (!res.ok) {
-        this.$msg.error({
-          message: res.message,
-          duration: 0
-        });
+        this.hint = res.message;
         return;
       }
 
-      console.log('123131');
+
       this.doorNavigated = false;
       this.isDoorOpening = false;
       this.$nextTick(() => {
@@ -147,6 +151,12 @@ export default {
 </script>
 
 <style scoped>
+.login-bg {
+  background: url('@/assets/images/login-background.png') no-repeat center/cover;
+  width: 100%;
+  height: 100%;
+}
+
 .page{
   width: 100vw; height: 100vh;
   position: relative;
@@ -154,38 +164,6 @@ export default {
   --s: 1;
  }
 
-.bg{
-  position:absolute;
-  inset:0;
-  background: url("~@/assets/ai_bg.png") center/cover no-repeat;
-  filter: saturate(1.05);
-  z-index: 0;
-}
-
-.header-deco{
-  position:absolute;
-  top: 0; left: 0; right: 0;
-  height: calc(var(--s) * 86px);
-  background: url("~@/assets/header_deco1.png") center/cover no-repeat;
-  pointer-events:none;
-  opacity: 0.95;
-  z-index: 2;
-}
-
-.top-title{
-  position:absolute;
-  top: calc(var(--s) * 20px);
-  left: 50%;
-  transform: translateX(-50%);
-  font-size: calc(var(--s) * 24px);
-  letter-spacing: calc(var(--s) * 3px);
-  font-weight: 700;
-  color: #e6f6ff;
-  text-shadow: 0 0 calc(var(--s) * 12px) rgba(80,200,255,0.45);
-  pointer-events:none;
-  z-index: 3; /* 关键:必须压过 content */
-}
-
 .content{
   position:absolute;
   inset: 0;
@@ -261,10 +239,10 @@ export default {
 
 /* 按你给的比例调过 */
 
-.icon-1{ left: 16.8%; top: 81%; }   /* 盾牌 */
-.icon-2{ left: 41.5%; top: 78%; }   /* 人像 */
-.icon-3{ left: 61.5%;top: 68.5%;} /* 中间图标 */
-.icon-4{ left: 80.5%; top: 52%;}   /* 齿轮 */
+.icon-1{ left: 16.8%; top: 87%; }   /* 盾牌 */
+.icon-2{ left: 41.5%; top: 84%; }   /* 人像 */
+.icon-3{ left: 61.5%;top: 73%;} /* 中间图标 */
+.icon-4{ left: 80.5%; top: 56%;}   /* 齿轮 */
 
 
 /* 单独给每个球不同浮动幅度(可选,效果更高级) */
@@ -424,8 +402,11 @@ export default {
 
 .hint{
   margin-top: calc(var(--s) * 12px);
-  color: rgba(255,180,180,0.92);
+  margin-bottom: calc(var(--s) * 12px);
+  color: rgba(255, 0, 0, 0.92);
   font-size: var(--fs-base);
+  width: 85%;
+  text-align: left;
 }
 
 /* 响应式重排:窄屏时上下布局 */
@@ -445,7 +426,7 @@ export default {
   z-index: 1; /* 在 bg(0) 上面,但不压住 header/title 可再调 */
   
   /* ✅必须和 .bg 使用同一张图 + 同样的 size/position,才能精确对齐 */
-  background-image: url('~@/assets/ai_bg.png'); /* ← 改成你真实路径 */
+  background-image: url('~@/assets/images/login-background.png'); /* ← 改成你真实路径 */
   background-repeat: no-repeat;
   background-size: cover;
   background-position: center center;
@@ -508,7 +489,7 @@ export default {
   position: absolute;
   inset: 0;
 
-  background: url("~@/assets/ai_bg.png") center/cover no-repeat; /* 跟 .bg 完全一致 */
+  background: url("~@/assets/images/login-background.png") center/cover no-repeat; /* 跟 .bg 完全一致 */
   filter: brightness(1.03) contrast(1.04);
   will-change: transform;
 }
@@ -604,34 +585,15 @@ export default {
 .copyright {
   display: flex;
   align-items: center;
+  flex-direction: column;
   font-size: 18px;
   color: #FFF;
   line-height: 30px;
   text-align: center;
   position: absolute;
   bottom: 20px;
-  left: calc(50% - 35px);
-}
-
-.copyright::before,
-.copyright::after {
-  content: "";
-  display: block;
-  width: 4px;
-  height: 4px;
-  border-radius: 2px;
-  background: #e7e7e7;
-}
-
-.copyright::before {
-  margin-right: 10px;
-}
-
-.copyright::after {
-  margin-left: 10px;
-}
-.copyright-logo {
-  width: 212.52px;
-  height: 43.5px;
+  left: 0;
+  justify-content: center;
+  width: 100%;
 }
 </style>

+ 25 - 0
src/views/SpecialSituationMonitoring.vue

@@ -14,6 +14,8 @@
             <TongzhouTrafficMap
             amapKey="db2da7e3e248c3b2077d53fc809be63f"
             securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
+            :mode="activeLeftTab === 'crossing' ? '路口' : activeLeftTab === 'trunkLine' ? '干线' : activeLeftTab === 'specialDuty' ? '特勤' : ''"
+            @map-crossing-click="handleMapCrossingClick"
             />
         </template>
 
@@ -214,6 +216,29 @@ export default {
             this.testOpenSecurityRoute();
             this.testOpenSecurityRoute2();
         },
+        // 处理地图点击事件
+        handleMapCrossingClick(mapData, lnglat) {
+            console.log('父组件接收到了地图路口点击事件:', mapData, lnglat);
+            // 组装模拟数据
+            let nodeData = {
+                id: Math.random(1, 100),
+                label: mapData.road,
+            }
+            console.log(nodeData);
+            if (this.activeLeftTab === 'overview') { // 总览
+                this.testOpenSecurityRoute();
+                this.testOpenSecurityRoute2();
+            } else if (this.activeLeftTab === 'crossing') { // 路口
+                this.testOpenSecurityRoute();
+                this.testOpenSecurityRoute2();
+            } else if (this.activeLeftTab === 'trunkLine') { // 干线
+                this.testOpenSecurityRoute();
+                this.testOpenSecurityRoute2();
+            } else if (this.activeLeftTab === 'specialDuty') { // 特勤
+                this.testOpenSecurityRoute();
+                this.testOpenSecurityRoute2();
+            }
+        },
         // ================= 测试用例:模拟各种点击行为 =================
 
         // 模拟 1:打开特勤安保路线面板

+ 106 - 44
src/views/StatusMonitoring.vue

@@ -9,14 +9,23 @@
         </template>
 
         <template #map>
+            <!-- 路口列表 -->
+            <div v-if="currentView === 'list-mode' && activeLeftTab === 'crossing'" class="list-mode-panel">
+                <CrossingListPanel :onViewDetail="handleCrossingViewDetail"/>
+            </div>
             <!-- 地图 -->
-            <TongzhouTrafficMap amapKey="db2da7e3e248c3b2077d53fc809be63f"
-                securityJsCode="a7413c674852c5eaf01d90813c5b7ef6" @map-crossing-click="handleMapCrossingClick" />
+            <TongzhouTrafficMap v-else
+                amapKey="db2da7e3e248c3b2077d53fc809be63f"
+                securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
+                :mode="activeLeftTab === 'crossing' ? '路口' : activeLeftTab === 'trunkLine' ? '干线' : activeLeftTab === 'specialDuty' ? '特勤' : ''"
+                @map-crossing-click="handleMapCrossingClick"
+            />
         </template>
 
+
         <template #left>
             <!-- 左侧Tab菜单栏 -->
-            <div class="left-sidebar-wrap">
+            <div class="left-sidebar-wrap" v-if="currentView !== 'list-mode'">
                 <TechTabs v-model="activeLeftTab" type="underline" @tab-click="handleTabClick">
                     <TechTabPane label="总览" name="overview" class="menu-scroll-view">
                         <MenuItem theme="tech" v-for="item in menuData" :key="item.id" :node="item" :level="0"
@@ -46,6 +55,28 @@
             <div class="mode-switch" v-if="activeLeftTab === 'crossing'">
                 <ButtonGroup v-model="currentView" :options="viewOptions" @select="onViewSelect" />
             </div>
+            <!-- 特勤右上角表格 -->
+            <TechTable ref="dutyTable" :columns="tableColumns" :data="tableData" class="duty-table" v-if="activeLeftTab === 'specialDuty'">
+
+                <template #level="{ row }">
+                    <span :title="row.level" :style="{ color: row.level === '二级' ? '#FFDF0C' : '#F00' }">
+                        {{ row.level }}
+                    </span>
+                </template>
+
+                <template #status="{ row }">
+                    <span :title="row.status" :style="{ color: row.status === '进行中' ? '#FFDF0C' : '#F00' }">
+                        {{ row.status }}
+                    </span>
+                </template>
+
+                <template #action="{ row }">
+                    <span class="action-btn" @click="handleSpecialTaskView(row)">
+                        查看
+                    </span>
+                </template>
+
+            </TechTable>
         </template>
 
         <template #center>
@@ -70,6 +101,8 @@ import arrow1 from '@/assets/images/arrow_1.png';
 import arrow2 from '@/assets/images/arrow_2.png';
 import arrow3 from '@/assets/images/arrow_3.png';
 import arrow4 from '@/assets/images/arrow_4.png';
+import TechTable from '@/components/ui/TechTable.vue';
+import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 
 
 export default {
@@ -82,6 +115,8 @@ export default {
         TongzhouTrafficMap,
         MenuItem,
         ButtonGroup,
+        TechTable,
+        CrossingListPanel
     },
     data() {
         return {
@@ -214,7 +249,25 @@ export default {
             viewOptions: [
                 { label: '列表模式', value: 'list-mode' },
                 { label: '地图模式', value: 'map-mode' },
-            ]
+            ],
+            // 1. 表头
+            tableColumns: [
+                { label: '序号', key: 'id', width: '14%' },
+                { label: '名称', key: 'name', width: '20%' },
+                { label: '执行人', key: 'executor', width: '18%' },
+                { label: '等级', key: 'level', width: '14%' },
+                { label: '状态', key: 'status', width: '20%' },
+                { label: '操作', key: 'action', width: '14%' }
+            ],
+
+            // 2. 模拟数据源
+            tableData: [
+                { id: 1, name: '大型活动交通安保', executor: '测试', level: '一级', status: '未开始' },
+                { id: 2, name: '道路施工路段交通引导张飞', executor: '张飞', level: '一级', status: '未开始' },
+                { id: 3, name: '酒驾醉驾专项查缉', executor: '关将', level: '二级', status: '进行中' },
+                { id: 4, name: '交通信号灯故障排查', executor: '刘备', level: '一级', status: '未开始' },
+                { id: 5, name: '应急救援通道清障', executor: '孙权', level: '一级', status: '未开始' }
+            ],
         };
     },
     watch: {
@@ -266,21 +319,21 @@ export default {
             this.$refs.layout.clearDialogs(); // 清空全部弹窗
             // 列表模式弹窗
             if (this.currentView === 'list-mode') {
-                this.$refs.layout.openDialog({
-                    id: 'crossing-list', // 这里的 ID 可以根据实际业务场景动态生成
-                    title: '',
-                    component: 'CrossingListPanel',
-                    width: 1920,
-                    height: 750,
-                    center: false,
-                    showClose: true,
-                    noPadding: false,
-                    enableDblclickExpand: false,
-                    position: { x: 100, y: 150 },
-                    data: {
-                        onViewDetail: (rowData) => this.handleCrossingViewDetail(rowData)
-                    }
-                });
+                // this.$refs.layout.openDialog({
+                //     id: 'crossing-list', // 这里的 ID 可以根据实际业务场景动态生成
+                //     title: '',
+                //     component: 'CrossingListPanel',
+                //     width: 1920,
+                //     height: 750,
+                //     center: false,
+                //     showClose: true,
+                //     noPadding: false,
+                //     enableDblclickExpand: false,
+                //     position: { x: 100, y: 150 },
+                //     data: {
+                //         onViewDetail: (rowData) => this.handleCrossingViewDetail(rowData)
+                //     }
+                // });
             } else {
                 this.showCrossingTopDialogs();
             }
@@ -309,7 +362,7 @@ export default {
         // 处理弹窗双击展开(通过 onExpand 回调从 Layout 传入)
         handleDoubleClickExpend(nodeData) {
             console.log('处理弹窗双击事件', nodeData);
-            if (this.activeLeftTab === 'crossing') {
+            if (this.activeLeftTab === 'crossing' || this.activeLeftTab === 'overview') {
                 this.showCrossingDetailDialogs(nodeData);
             }
         },
@@ -328,7 +381,26 @@ export default {
         // 显示总览弹窗组
         showOverviewDalogs(nodeData) {
             console.log('显示总览弹窗组', nodeData.id, nodeData.label);
-            this.showCrossingDetailDialogs(nodeData);
+            // 路口弹窗
+            this.$refs.layout.openDialog({
+                id: 'crossing3_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
+                title: nodeData.label,
+                component: 'CrossingPanel',
+                width: 260,
+                height: 260,
+                center: false,
+                showClose: true,
+                position: { x: 950, y: 430 },
+                noPadding: false,
+                data: {
+                    ...nodeData,
+                    onExpand: (data) => this.handleDoubleClickExpend(data)
+                },
+                onClose: () => {
+                    // this.$refs.layout.handleDialogClose('top-chart-crossing-1');
+                    // this.$refs.layout.handleDialogClose('top-chart-crossing-2');
+                }
+            });
         },
         showOverviewTopDialogs() {
             this.$refs.layout.openDialog({
@@ -364,34 +436,13 @@ export default {
         showCrossingDalogs(nodeData) {
             console.log('显示路口弹窗组', nodeData.id, nodeData.label);
 
-            // 路口弹窗
-            // this.$refs.layout.openDialog({
-            //     id: 'crossing3_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
-            //     title: nodeData.label,
-            //     component: 'CrossingPanel',
-            //     width: 260,
-            //     height: 260,
-            //     center: false,
-            //     showClose: true,
-            //     position: { x: 950, y: 430 },
-            //     noPadding: false,
-            //     data: {
-            //         ...nodeData,
-            //         onExpand: (data) => this.handleDoubleClickExpend(data)
-            //     },
-            //     onClose: () => {
-            //         this.$refs.layout.handleDialogClose('top-chart-crossing-1');
-            //         this.$refs.layout.handleDialogClose('top-chart-crossing-2');
-            //     }
-            // });
-
             this.showCrossingDetailDialogs(nodeData);
 
 
         },
 
         showCrossingDetailDialogs(nodeData) {
-            console.log('显示干线弹窗组', nodeData.id, nodeData.label);
+            console.log('显示路口详情弹窗组', nodeData.id, nodeData.label);
             this.$refs.layout.openDialog({
                 id: 'crossing_detail' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
                 title: nodeData.label || nodeData.name,
@@ -531,7 +582,7 @@ export default {
                 width: 1400, // 弹窗宽一点,容纳 3 列
                 height: 700,
                 center: false,
-                showClose: false,
+                showClose: true,
                 noPadding: true, // 去除默认内边距,让内部组件自己控制
                 position: {x: 200, y: 150},
                 // 挂载主体组件和数据
@@ -548,6 +599,11 @@ export default {
                 }
             });
         },
+        handleSpecialTaskView(row) {
+            console.log('查看特勤线路,当前数据:', row);
+            this.openDutyDetailDialog(row);
+        
+        },
 
         // 模拟从后端拉取数据
         async fetchSpecialTaskData() {
@@ -658,4 +714,10 @@ export default {
 .mode-switch>div {
     width: 200px;
 }
+.duty-table {
+    margin-top: 10px;
+}
+::v-deep .list-mode-panel {
+    padding: 150px 30px 0 30px;
+}
 </style>