Selaa lähdekoodia

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

hebotao 3 viikkoa sitten
vanhempi
commit
a918c0b4d5

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8536 - 0
pnpm-lock.yaml


BIN
src/assets/map/direction.png


+ 224 - 130
src/components/TongzhouTrafficMap.vue

@@ -60,7 +60,6 @@ export default {
 
 
       routeGroups: {},
-      polylines: [],
       privateStyle: {
         legend: {}
       },
@@ -349,6 +348,7 @@ export default {
         // 2. 处理路线类(干线/特勤)
         this.routeGroups[config.name] = [];
         const lines = realRouteConfigs[config.name] || [];
+        const trunkSegments = [];
 
         for (let lineIdx = 0; lineIdx < lines.length; lineIdx += 1) {
           if (this.isComponentDestroyed || drawSeq !== this.drawSeq) return;
@@ -369,6 +369,22 @@ export default {
             path
           });
 
+          // 统计干线协调实际生成的线段数
+          if (config.name === '干线协调' && overlays.length > 0) {
+            const polylineCount = overlays.filter(o => o instanceof this.AMap.Polyline).length;
+            for (let s = 0; s < polylineCount; s++) {
+              const idx = trunkSegments.length + 1;
+              trunkSegments.push({
+                id: 'trunk_' + idx,
+                label: '干线' + idx,
+                intersections: Array.from({ length: 6 }, (_, k) => '干线' + idx + '_路口' + (k + 1)),
+                distances: Array.from({ length: 6 }, (_, k) => k * 1000),
+                _lineIdx: lineIdx,
+                _segmentIdx: s
+              });
+            }
+          }
+
           if (this.isComponentDestroyed || drawSeq !== this.drawSeq) return;
 
           if (overlays.length > 0) {
@@ -378,6 +394,11 @@ export default {
 
           await this.sleep(80);
         }
+
+        // 干线协调绘制完成后,通知父组件用于菜单渲染
+        if (config.name === '干线协调' && trunkSegments.length > 0) {
+          this.$emit('bindTrunkMenuTree', trunkSegments);
+        }
       }
     },
 
@@ -463,11 +484,24 @@ export default {
         basePath = this.buildFallbackLinePath(line.start, line.end, 30);
       }
 
+      // --- 核心优化:物理分散偏移 (Dispersal Offset) ---
+      // 根据 lineIdx 为每条路径应用微小偏移,避免多条线路完全重合
+      // 0.00015 度约等于 15 米,(lineIdx - 3) 将多条线在中心点两侧排开
+      const offsetVal = (Number(lineIdx) - 3) * 0.00015;
+      const applyOffset = (p) => {
+        const lng = p.lng || (p.getLng ? p.getLng() : (Array.isArray(p) ? Number(p[0]) : 0));
+        const lat = p.lat || (p.getLat ? p.getLat() : (Array.isArray(p) ? Number(p[1]) : 0));
+        return [lng + offsetVal, lat + offsetVal];
+      };
+
       const segments = this.extractMainStraightSegments(basePath);
       const overlays = [];
 
-      segments.forEach((segmentPath, segmentIdx) => {
-        if (!Array.isArray(segmentPath) || segmentPath.length < 2) return;
+      segments.forEach((rawSegmentPath, segmentIdx) => {
+        if (!Array.isArray(rawSegmentPath) || rawSegmentPath.length < 2) return;
+
+        // 应用偏移到当前段的所有点
+        const segmentPath = rawSegmentPath.map(p => applyOffset(p));
 
         const polyline = new this.AMap.Polyline({
           path: segmentPath,
@@ -479,14 +513,72 @@ export default {
         overlays.push(polyline);
 
         const totalPoints = segmentPath.length;
-        const indices = this.pickEvenlySpacedIndices(totalPoints, 8);
+
+        // --- 核心优化:全路段物理距离均匀放置方向箭头 ---
+        // 1. 计算整条 segmentPath 的总物理距离及每个点的累计距离
+        const pathDistances = [0];
+        let totalPathDist = 0;
+        for (let j = 0; j < totalPoints - 1; j++) {
+          const d = this.calcApproxDistance(segmentPath[j], segmentPath[j+1]);
+          totalPathDist += d;
+          pathDistances.push(totalPathDist);
+        }
+
+        // 2. 设定标准间距:约每 0.0018 度 (约 200 米) 放置一个箭头
+        const targetSpacing = 0.0018;
+        let currentTargetDist = targetSpacing / 2; // 第一个箭头放在 1/2 间距处,让分布更美观
+
+        while (currentTargetDist < totalPathDist) {
+          // 3. 寻找对应 targetDist 的路径位置 (线性插值)
+          let foundIdx = 0;
+          for (let j = 0; j < pathDistances.length - 1; j++) {
+            if (currentTargetDist >= pathDistances[j] && currentTargetDist <= pathDistances[j + 1]) {
+              foundIdx = j;
+              break;
+            }
+          }
+
+          const p1 = segmentPath[foundIdx];
+          const p2 = segmentPath[foundIdx + 1];
+          if (p1 && p2) {
+            // 在 p1 和 p2 之间线性插值
+            const ratio = (currentTargetDist - pathDistances[foundIdx]) / (pathDistances[foundIdx + 1] - pathDistances[foundIdx]);
+            const lng1 = Number(p1[0]);
+            const lat1 = Number(p1[1]);
+            const lng2 = Number(p2[0]);
+            const lat2 = Number(p2[1]);
+            
+            const midLng = lng1 + (lng2 - lng1) * ratio;
+            const midLat = lat1 + (lat2 - lat1) * ratio;
+
+            const bearing = this.calcBearingDeg(p1, p2);
+            const rotation = bearing - 90;
+
+            const directionMarker = new this.AMap.Marker({
+              position: [midLng, midLat],
+              content: `
+                <div style="transform: rotate(${rotation}deg); width: 20px; height: 10px; display: flex; align-items: center; pointer-events: none; opacity: 0.85;">
+                  <img src="${require('@/assets/map/direction.png')}" style="width: 100%; height: auto;" />
+                </div>
+              `,
+              offset: new this.AMap.Pixel(-10, -5),
+              zIndex: 20,
+              bubble: true
+            });
+            overlays.push(directionMarker);
+          }
+          currentTargetDist += targetSpacing;
+        }
+
+        // 为圆点保留原来的分布 logic (不受箭头影响)
+        const indices = this.pickEvenlySpacedIndices(totalPoints, 6);
 
         // 为第一个和最后一个圆点设置特殊类型
         for (let i = 0; i < indices.length; i++) {
           const idx = indices[i];
           const p = segmentPath[idx];
-          const lng = p && typeof p.lng === 'number' ? p.lng : (Array.isArray(p) ? Number(p[0]) : NaN);
-          const lat = p && typeof p.lat === 'number' ? p.lat : (Array.isArray(p) ? Number(p[1]) : NaN);
+          const lng = Number(p[0]);
+          const lat = Number(p[1]);
           if (Number.isNaN(lng) || Number.isNaN(lat)) continue;
 
           // 确定圆点类型:第一个为start,最后一个为end,其余为normal
@@ -512,18 +604,11 @@ export default {
     pickEvenlySpacedIndices(totalPoints, count) {
       const total = Math.max(Number(totalPoints) || 0, 0);
       const target = Math.max(Number(count) || 0, 0);
-      if (total <= 0) return [];
-      if (target <= 0) return [];
+      if (total <= 0 || target <= 0) return [];
       if (target >= total) return Array.from({ length: total }, (_, i) => i);
       if (target === 1) return [0];
 
-      const set = new Set();
-      const last = total - 1;
-      for (let k = 0; k < target; k += 1) {
-        const idx = Math.round((k * last) / (target - 1));
-        set.add(idx);
-      }
-      return Array.from(set).sort((a, b) => a - b);
+      return Array.from({ length: target }, (_, k) => Math.round((k * (total - 1)) / (target - 1)));
     },
 
     buildFallbackLinePath(start, end, pointCount) {
@@ -546,7 +631,14 @@ export default {
     },
 
     extractMainStraightSegments(path) {
-      const points = (path || []).filter(p => p && typeof p.lng === 'number' && typeof p.lat === 'number');
+      const getCoord = (p) => {
+        if (!p) return { lng: NaN, lat: NaN };
+        if (Array.isArray(p)) return { lng: Number(p[0]), lat: Number(p[1]) };
+        if (p.getLng) return { lng: p.getLng(), lat: p.getLat() };
+        return { lng: Number(p.lng), lat: Number(p.lat) };
+      };
+
+      const points = (path || []).map(p => getCoord(p)).filter(p => !isNaN(p.lng) && !isNaN(p.lat));
       if (points.length < 2) return [];
 
       const thresholdDeg = 18;
@@ -600,13 +692,24 @@ export default {
       return finalSegments.length > 0 ? finalSegments : [points];
     },
 
+    // 内部通用工具:从不同格式的点中提取经纬度数组 [lng, lat]
+    _getCoords(p) {
+      if (!p) return [0, 0];
+      const lng = p.lng || (p.getLng ? p.getLng() : (Array.isArray(p) ? Number(p[0]) : 0));
+      const lat = p.lat || (p.getLat ? p.getLat() : (Array.isArray(p) ? Number(p[1]) : 0));
+      return [lng, lat];
+    },
+
     calcBearingDeg(a, b) {
-      const latRad = ((a.lat + b.lat) / 2) * Math.PI / 180;
-      const dx = (b.lng - a.lng) * Math.cos(latRad);
-      const dy = (b.lat - a.lat);
-      let deg = Math.atan2(dy, dx) * 180 / Math.PI;
-      if (deg < 0) deg += 360;
-      return deg;
+      const [alng, alat] = this._getCoords(a);
+      const [blng, blat] = this._getCoords(b);
+
+      const latRad = ((alat + blat) / 2) * Math.PI / 180;
+      const dx = (blng - alng) * Math.cos(latRad);
+      const dy = (blat - alat);
+      
+      const mathAngle = Math.atan2(dy, dx) * 180 / Math.PI;
+      return (90 - mathAngle + 360) % 360;
     },
 
     calcAngleDiffDeg(a, b) {
@@ -616,9 +719,12 @@ export default {
     },
 
     calcApproxDistance(a, b) {
-      const latRad = ((a.lat + b.lat) / 2) * Math.PI / 180;
-      const dx = (b.lng - a.lng) * Math.cos(latRad);
-      const dy = (b.lat - a.lat);
+      const [alng, alat] = this._getCoords(a);
+      const [blng, blat] = this._getCoords(b);
+
+      const latRad = ((alat + blat) / 2) * Math.PI / 180;
+      const dx = (blng - alng) * Math.cos(latRad);
+      const dy = (blat - alat);
       return Math.sqrt(dx * dx + dy * dy);
     },
 
@@ -626,8 +732,8 @@ export default {
       if (!position || !config) return null;
 
       try {
-        const lng = Number(position[0] || position.lng);
-        const lat = Number(position[1] || position.lat);
+        const lng = position.getLng ? position.getLng() : Number(position[0] !== undefined ? position[0] : position.lng);
+        const lat = position.getLat ? position.getLat() : Number(position[1] !== undefined ? position[1] : position.lat);
         if (isNaN(lng) || isNaN(lat)) return null;
 
         // 状态文字:起、终、或者状态配置的首字母
@@ -637,99 +743,58 @@ export default {
 
         const isAbnormal = ["离线", "降级", "故障"].includes(config.name);
         const isRoute = ["干线协调", "勤务路线"].includes(config.name);
+        const isStartEnd = type === 'start' || type === 'end';
+
+        // 核心配置映射:减少嵌套逻辑
+        const markerStyle = isStartEnd ? {
+          size: '24px',
+          height: '30px',
+          offset: [-12, -30],
+          zIndex: 120,
+          border: '2px solid #fff'
+        } : (isAbnormal ? {
+          size: '30px',
+          height: '30px',
+          offset: [-15, -15],
+          zIndex: 110,
+          border: 'none'
+        } : {
+          size: '14px',
+          height: '14px',
+          offset: [-9, -9], // 14px + 2px padding * 2 = 18px total
+          zIndex: 100,
+          border: isRoute ? 'none' : '1.5px solid rgba(255,255,255,0.7)'
+        });
 
-      // 动态计算尺寸:起终点最大,异常点次之,普通点最小
-      const isStartEnd = type === 'start' || type === 'end';
-      const size = isStartEnd ? '24px' : (isAbnormal ? '30px' : '14px');
-      const sizeNumber = parseInt(size, 10);
-      const paddingNumber = (isAbnormal || isStartEnd) ? 0 : 2;
-      const outerSizeNumber = sizeNumber + paddingNumber * 2;
-
-      // 边框样式:起终点用实白边框
-      const borderStyle = isStartEnd
-        ? '2px solid #fff'
-        : '1.5px solid rgba(255,255,255,0.7)';
-
-      // 生成标记内容
-      let markerContent = '';
-      if (isStartEnd) {
-        const iconPath = type === 'start' 
-          ? require('@/assets/map/start.png') 
-          : require('@/assets/map/end.png');
-        markerContent = `
-          <div class="pure-light-node start-end-node"
-              style="
-                width: ${size};
-                height: 30px;
-                background: transparent;
-                border: none;
-                display: flex;
-                justify-content: center;
-                align-items: flex-end;
-                cursor: pointer;
-                transform-origin: bottom center;
-              ">
-            <img src="${iconPath}" style="width: 100%; height: auto; object-fit: contain; pointer-events: none;" />
-          </div>
-        `;
-      } else if (isAbnormal) {
-        const iconName = config.name === '离线' ? 'lixian' : config.name === '降级' ? 'jiangji' : 'guzhang';
-        markerContent = `
-          <div class="pure-light-node ${isAbnormal ? 'breathe' : ''}"
-              style="
-                width: ${size};
-                height: ${size};
-                background: transparent;
-                box-shadow: none;
-                border: none;
-                box-sizing: content-box;
-                display: flex;
-                justify-content: center;
-                align-items: center;
-                cursor: pointer;
-                padding: ${paddingNumber}px;
-              ">
-            <img src="${require(`@/assets/images/icon_${iconName}.png`)}" style="width: 100%; height: 100%; object-fit: contain;" />
-          </div>
-        `;
-      } else {
-        markerContent = `
-          <div class="pure-light-node ${isAbnormal ? 'breathe' : ''} ${isRoute ? 'route-node' : ''}"
-              style="
-                width: ${size};
-                height: ${size};
-                background: ${config.color || '#999'};
-                box-shadow: ${isRoute ? 'none' : `0 0 8px ${config.color}`};
-                border: ${isRoute ? 'none' : borderStyle};
-                box-sizing: content-box;
-                display: flex;
-                justify-content: center;
-                align-items: center;
-                color: #fff;
-                border-radius: 50%;
-                cursor: pointer;
-                padding: ${paddingNumber}px;
-              ">
-            <span style="transform: scale(0.8); font-weight: bold; font-size: 12px;">${displayText}</span>
-          </div>
-        `;
-      }
-
-      // 计算偏移量:起点终点底部对齐,其他中心对齐
-      let markerOffset;
-      if (isStartEnd) {
-        // 设置 24px 宽 30px 高,偏移量设置为底部中心对齐
-        markerOffset = new this.AMap.Pixel(-12, -30);
-      } else {
-        markerOffset = new this.AMap.Pixel(-Math.round(outerSizeNumber / 2), -Math.round(outerSizeNumber / 2));
-      }
+        // 生成标记内容
+        let markerContent = '';
+        if (isStartEnd) {
+          markerContent = `
+            <div class="pure-light-node start-end-node" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: transparent; border: none; display: flex; justify-content: center; align-items: flex-end; cursor: pointer; transform-origin: bottom center;">
+              <img src="${require(`@/assets/map/${type}.png`)}" style="width: 100%; height: auto; object-fit: contain; pointer-events: none;" />
+            </div>
+          `;
+        } else if (isAbnormal) {
+          const iconName = config.name === '离线' ? 'lixian' : config.name === '降级' ? 'jiangji' : 'guzhang';
+          markerContent = `
+            <div class="pure-light-node breathe" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: transparent; border: none; box-sizing: content-box; display: flex; justify-content: center; align-items: center; cursor: pointer; padding: 0;">
+              <img src="${require(`@/assets/images/icon_${iconName}.png`)}" style="width: 100%; height: 100%; object-fit: contain;" />
+            </div>
+          `;
+        } else {
+          markerContent = `
+            <div class="pure-light-node ${isRoute ? 'route-node' : ''}" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: ${config.color || '#999'}; box-shadow: ${isRoute ? 'none' : `0 0 8px ${config.color}`}; border: ${markerStyle.border}; box-sizing: content-box; display: flex; justify-content: center; align-items: center; color: #fff; border-radius: 50%; cursor: pointer; padding: 2px;">
+              <span style="transform: scale(0.8); font-weight: bold; font-size: 12px;">${displayText}</span>
+            </div>
+          `;
+        }
 
-      const marker = new this.AMap.Marker({
-        position: [lng, lat],
-        zIndex: isStartEnd ? 120 : (isAbnormal ? 110 : 100),
-        content: markerContent,
-        offset: markerOffset,
-        extData: {
+        const marker = new this.AMap.Marker({
+          position: [lng, lat],
+          zIndex: markerStyle.zIndex,
+          content: markerContent,
+          offset: new this.AMap.Pixel(...markerStyle.offset),
+          extData: {
             ...config,
             position: [lng, lat],
             statusColor: config.color || '#999',
@@ -827,7 +892,8 @@ export default {
       if (!this.infoWindow) {
         this.infoWindow = new this.AMap.InfoWindow({
           isCustom: true,
-          offset: new this.AMap.Pixel(0, -20)
+          offset: new this.AMap.Pixel(0, -20),
+          autoMove: false // 防止弹窗自动平移导致的中心点偏移
         });
       }
 
@@ -934,27 +1000,55 @@ export default {
     },
 
     focusByLocation(targetPos) {
-      if (!this.isMapReady() || !targetPos || targetPos.length !== 2) return;
+      if (!this.isMapReady() || !targetPos) return;
 
-      let foundMarker = null;
+      // 如果是字符串坐标 "lng,lat",则解析
+      let pos = targetPos;
+      if (typeof targetPos === 'string') {
+        pos = targetPos.split(',').map(Number);
+      }
+      if (!Array.isArray(pos) || pos.length < 2) return;
+
+      const [targetLng, targetLat] = pos;
+      let bestMarker = null;
+      let minDistanceSq = Infinity;
+
+      // 遍历所有路由组,寻找离坐标点最近的标记
       Object.values(this.routeGroups).forEach(group => {
-        const marker = group.find(item => {
-          if (!(item instanceof this.AMap.Marker)) return false;
-          const pos = item.getExtData().position;
-          return Math.abs(pos[0] - targetPos[0]) < 0.0001 && Math.abs(pos[1] - targetPos[1]) < 0.0001;
+        if (!Array.isArray(group)) return;
+        group.forEach(item => {
+          if (!(item instanceof this.AMap.Marker)) return;
+          const markerExt = item.getExtData();
+          const markerPos = markerExt.position;
+          if (!markerPos) return;
+
+          const dx = markerPos[0] - targetLng;
+          const dy = markerPos[1] - targetLat;
+          const distSq = dx * dx + dy * dy;
+          
+          // 容差范围内(约 20 米),寻找最接近的点
+          if (distSq < 0.000001) {
+            // 优先规则:如果距离相同或非常接近,优先选择异常状态点(离线/降级/故障)
+            const isAbnormal = ["离线", "降级", "故障"].includes(markerExt.name);
+            const currentIsAbnormal = bestMarker ? ["离线", "降级", "故障"].includes(bestMarker.getExtData().name) : false;
+            
+            if (distSq < minDistanceSq || (isAbnormal && !currentIsAbnormal)) {
+              minDistanceSq = distSq;
+              bestMarker = item;
+            }
+          }
         });
-        if (marker) foundMarker = marker;
       });
 
-      if (foundMarker) {
-        const finalPos = foundMarker.getPosition();
+      if (bestMarker) {
+        const finalPos = bestMarker.getPosition();
         this.map.setZoomAndCenter(17, finalPos, false, 500);
         setTimeout(() => {
-          if (!this.isComponentDestroyed) this.openLightInfo(foundMarker.getExtData(), finalPos);
+          if (!this.isComponentDestroyed) this.openLightInfo(bestMarker.getExtData(), finalPos);
         }, 600);
       } else {
         // 如果找不到对应的标记,直接使用传入的坐标设置地图中心
-        this.map.setZoomAndCenter(17, targetPos, false, 500);
+        this.map.setZoomAndCenter(17, [targetLng, targetLat], false, 500);
       }
     },
 

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

@@ -7,7 +7,7 @@
         <div class="alarm-item" v-for="(item, index) in listData" :key="item.id + '-' + index">
             <div class="item-header">
                 <span class="title" :class="getTitleClass(item.type)">
-                    {{index + 1}}.{{ item.title }}
+                    {{ (item._originalIndex != null ? item._originalIndex : index) + 1 }}.{{ item.title }}
                 </span>
                 <span class="time" v-if="item.time">{{ item.time }}</span>
             </div>

+ 8 - 3
src/components/ui/SeamlessScroll.vue

@@ -25,14 +25,19 @@ export default {
     scrollData() {
       if (this.data && this.data.length > this.limit) {
         this.isScrollable = true;
+        const original = this.data.map((item, index) => ({
+          ...item,
+          _originalIndex: index
+        }));
         const clone = JSON.parse(JSON.stringify(this.data)).map((item, index) => ({
           ...item,
-          _clone_id: `clone_${Date.now()}_${index}`
+          _clone_id: `clone_${Date.now()}_${index}`,
+          _originalIndex: index
         }));
-        return [...this.data, ...clone];
+        return [...original, ...clone];
       }
       this.isScrollable = false;
-      return this.data; 
+      return this.data.map((item, index) => ({ ...item, _originalIndex: index }));
     }
   },
   // 加入 mounted 钩子,确保 DOM 绝对渲染完毕再测算

+ 26 - 10
src/components/ui/SignalTimingChart.vue

@@ -108,7 +108,8 @@ export default {
     showAxis: { type: Boolean, default: true },
     showScanLine: { type: Boolean, default: true },
     showScanLineLabel: { type: Boolean, default: true },
-    autoScan: { type: Boolean, default: false }
+    autoScan: { type: Boolean, default: false },
+    syncScan: { type: Boolean, default: false }
   },
   data() {
     return { scaleFactor: 1, internalTime: 0 };
@@ -138,6 +139,9 @@ export default {
     autoScan(val) {
       if (val) { this.startAutoScan(); } else { this.stopAutoScan(); }
     },
+    syncScan() {
+      if (this.autoScan) { this.startAutoScan(); }
+    },
     showScanLine(val) {
       this.updateChart();
       if (val && this.autoScan) { this.startAutoScan(); }
@@ -154,19 +158,31 @@ export default {
     },
     startAutoScan() {
       this.stopAutoScan();
-      // 统一视觉周期:所有行在 VISUAL_PERIOD 秒内完成一次扫描
       const VISUAL_PERIOD = 120;
-      this._scanListener = (elapsed) => {
-        const realMax = this.getMaxTime();
-        const offset = this.currentTime || 0; // 动态读取,不用闭包捕获
-        const ratio = ((offset + elapsed) % VISUAL_PERIOD) / VISUAL_PERIOD;
-        this.internalTime = ratio * realMax;
-        if (this.$_chart) this.updateScanLine();
-      };
-      joinSharedTimer(this._scanListener, this.currentTime || 0);
+      if (this.syncScan) {
+        // 共享定时器:所有行扫描线完全同步
+        this._scanListener = (elapsed) => {
+          const realMax = this.getMaxTime();
+          const offset = this.currentTime || 0;
+          const ratio = ((offset + elapsed) % VISUAL_PERIOD) / VISUAL_PERIOD;
+          this.internalTime = ratio * realMax;
+          if (this.$_chart) this.updateScanLine();
+        };
+        joinSharedTimer(this._scanListener, this.currentTime || 0);
+      } else {
+        // 独立定时器:每行扫描线从 currentTime 位置开始独立移动
+        this.internalTime = this.currentTime || 0;
+        this._soloTimer = setInterval(() => {
+          const realMax = this.getMaxTime();
+          this.internalTime += 1;
+          if (this.internalTime > realMax) this.internalTime = 0;
+          if (this.$_chart) this.updateScanLine();
+        }, 1000);
+      }
     },
     stopAutoScan() {
       if (this._scanListener) { leaveSharedTimer(this._scanListener); this._scanListener = null; }
+      if (this._soloTimer) { clearInterval(this._soloTimer); this._soloTimer = null; }
     },
     initChart() {
       const chartDom = this.$refs.chartRef;

+ 4 - 4
src/components/ui/TrafficTimeSpace.vue

@@ -22,7 +22,7 @@ export default {
     downWaveColor: { type: String, default: 'rgba(104, 231, 95, 0.4)' },
     waveLabelColor: { type: String, default: '#e0f7fa' },
     // 【新增】扫描线颜色配置
-    timeLineColor: { type: String, default: '#FFD54F' }
+    timeLineColor: { type: String, default: '#1a9bff' }
   },
   data() {
     return {
@@ -256,7 +256,7 @@ export default {
         style: {
           stroke: this.timeLineColor, // 线条颜色
           lineWidth: px2echarts(5), // 线条粗细
-          lineDash: [px2echarts(5), px2echarts(5)] // 设置为虚线,如果想实线可以删掉这行
+          lineDash: null // 实线
         }
       };
     },
@@ -276,7 +276,7 @@ export default {
         elapsed += delta;
         speedChangeTimer += delta;
 
-        this.currentTimeIndicator += delta;
+        this.currentTimeIndicator += 1;
 
         // 在 viewWindow 范围内循环,循环时重置所有速度
         if (this.currentTimeIndicator > this.viewWindow) {
@@ -331,7 +331,7 @@ export default {
             });
           }
         }
-      }, 16);
+      }, 1000);
     },
 
     // 【新增】停止扫描线动画

+ 1 - 1
src/layouts/DashboardLayout.vue

@@ -156,7 +156,7 @@ export default {
     },
     data() {
         return {
-            title: '灵•智交通信号控制平台',
+            title: '交通信号控制平台—灵•智',
         }
     },
     methods: {

+ 1 - 1
src/layouts/LoginLayout.vue

@@ -42,7 +42,7 @@ export default {
     },
     data() {
         return {
-            title: '灵•智交通信号控制平台',
+            title: '交通信号控制平台—灵•智',
         }
     },
     methods: {

+ 1 - 1
src/mock/api.js

@@ -607,7 +607,7 @@ export async function apiGetCrossingList(params = {}) {
       status: statuses[Math.floor(seededRand(i + 42) * statuses.length)],
       cycle: cycleLength,
       phaseData,
-      currentTime: pageOffset,
+      currentTime: Math.floor(seededRand(i * 31 + page * 97) * cycleLength),
     }
   })
 

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 312 - 1036
src/mock/mock_data.json


+ 13 - 4
src/utils/cesiumPreloader.js

@@ -27,11 +27,20 @@ const CesiumPreloader = {
       animation: false, timeline: false, baseLayerPicker: false, geocoder: false,
       homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false,
       fullscreenButton: false, selectionIndicator: false, shadows: false, shouldAnimate: false,
-      imageryProvider: new Cesium.UrlTemplateImageryProvider({
-        url: './tiles/{z}/{y}/{x}.jpg',
-        maximumLevel: 12
-      })
+
+      // 关键:关闭默认底图,否则会覆盖
+      imageryProvider: false,
+      baseLayer: false
+    });
+
+    const GeoToken = '5f1c0062177713e6c61bc0b414367256';
+    // 加载 ArcGIS 卫星图 = 谷歌卫星同款清晰度,国内秒加载
+    const arcGIS_Satellite = new Cesium.UrlTemplateImageryProvider({
+      url: `https://api.open.geovisearth.com/map/v1/img/{z}/{x}/{y}?token=${GeoToken}&format=webp&tmsIds=w`,
+      tilingScheme: new Cesium.WebMercatorTilingScheme(),
+      maximumLevel: 19
     });
+    _viewer.imageryLayers.addImageryProvider(arcGIS_Satellite)
 
     _viewer.cesiumWidget.creditContainer.style.display = 'none';
 

+ 17 - 12
src/views/Home.vue

@@ -226,24 +226,29 @@ export default {
     // 处理查看逻辑
     onAlarmView({ item, index }) {
       console.log('点击了查看:', item);
-      // 处理索引,确保即使点击克隆数据也能正确映射到原始12个点位
-      const actualIndex = index % 12;
-      // 从localStorage获取对应的位置信息
-      const positionStr = localStorage.getItem(`pos${actualIndex + 1}`);
+
       let position;
-      
-      if (!positionStr) {
-        console.warn('未找到对应的位置信息,使用默认位置');
-        // 使用默认坐标(通州区中心)
-        position = ['116.663', '39.905'];
+
+      // 优先使用 item 自身携带的坐标(最可靠,不受索引偏移影响)
+      if (item.position && item.position.length === 2) {
+        position = [Number(item.position[0]), Number(item.position[1])];
       } else {
-        position = positionStr.split(',');
+        // 兜底:通过索引从 localStorage 查找
+        const actualIndex = index % 12;
+        const positionStr = localStorage.getItem(`pos${actualIndex + 1}`);
+        if (positionStr) {
+          const parts = positionStr.split(',');
+          position = [Number(parts[0]), Number(parts[1])];
+        } else {
+          console.warn('未找到对应的位置信息,使用默认位置');
+          position = [116.663, 39.905];
+        }
       }
 
       // 地图联动
       console.log(position);
-      
-      this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
+
+      this.$refs.trafficMapRef.focusByLocation(position);
 
     },
     onIntersectionRowClick({ row, index }) {

+ 50 - 5
src/views/SpecialSituationMonitoring.vue

@@ -21,7 +21,7 @@
                 @map-crossing-click="handleMapCrossingClick"
                 @map-crossing-mouseover="handleMapCrossingMouseover"
                 @map-crossing-mouseout="handleMapCrossingMouseout"
-
+                @bindTrunkMenuTree="handleTrunkMenuUpdate"
             />
         </template>
 
@@ -113,7 +113,7 @@ import TaskCardList from '@/components/ui/TaskCardList.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
-import { apiGetTongzhouMenuTree, apiGetTrunkLineMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
 
 
 export default {
@@ -175,13 +175,12 @@ export default {
     },
     async mounted() {
         // 加载菜单和任务数据
-        const [menuData, trunkData, taskData] = await Promise.all([
+        const [menuData, taskData] = await Promise.all([
             apiGetTongzhouMenuTree(),
-            apiGetTrunkLineMenuTree(),
             apiGetTasks({ pageSize: 5 }),
         ]);
         this.menuData = menuData || [];
-        this.trunkLineMenuData = trunkData || [];
+        this.trunkLineMenuData = [];
         this.tableData = taskData?.list || taskData || [];
 
         // 组件挂载时检查路由
@@ -234,6 +233,16 @@ export default {
                 pixelX: pixel ? Math.round(pixel.x / scale) : 950,
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
+            // 干线marker点击时,从菜单数据中匹配对应干线
+            if (this.activeLeftTab === 'trunkLine' && mapData.id && String(mapData.id).startsWith('MOCK-干')) {
+                const matched = this.findTrunkMenuNode(mapData.id);
+                if (matched) {
+                    nodeData.id = matched.id;
+                    nodeData.label = matched.label;
+                    nodeData.intersections = matched.intersections;
+                    nodeData.distances = matched.distances;
+                }
+            }
             console.log(nodeData);
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showCrossingDetailDialogs(nodeData);
@@ -466,6 +475,42 @@ export default {
             console.log('干线菜单点击:', nodeData);
             this.showTrunkLineDalogs(nodeData);
         },
+        findTrunkMenuNode(markerId) {
+            const parts = markerId.split('-');
+            const lineIdx = parseInt(parts[2], 10);
+            const segmentIdx = parseInt(parts[3], 10);
+            const leaves = [];
+            const walk = (nodes) => {
+                if (!Array.isArray(nodes)) return;
+                for (const n of nodes) {
+                    if (n.children && n.children.length > 0) walk(n.children);
+                    else leaves.push(n);
+                }
+            };
+            walk(this.trunkLineMenuData);
+            return leaves.find(n => n._lineIdx === lineIdx && n._segmentIdx === segmentIdx) || null;
+        },
+        handleTrunkMenuUpdate(segments) {
+            this.trunkLineMenuData = [{
+                id: 'trunk_root',
+                label: '主控中心',
+                icon: 'icon-control',
+                isOpen: true,
+                children: [{
+                    id: 'trunk_beijing',
+                    label: '北京市交警总队',
+                    icon: 'icon-police',
+                    isOpen: true,
+                    children: [{
+                        id: 'trunk_tongzhou',
+                        label: '通州区',
+                        icon: 'icon-district',
+                        isOpen: true,
+                        children: segments
+                    }]
+                }]
+            }];
+        },
         async showTrunkLineDalogs(nodeData) {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
             // 优先使用菜单节点自带的路口和距离数据

+ 50 - 5
src/views/StatusMonitoring.vue

@@ -21,7 +21,7 @@
                 @map-crossing-click="handleMapCrossingClick"
                 @map-crossing-mouseover="handleMapCrossingMouseover"
                 @map-crossing-mouseout="handleMapCrossingMouseout"
-
+                @bindTrunkMenuTree="handleTrunkMenuUpdate"
             />
         </template>
 
@@ -113,7 +113,7 @@ import TaskCardList from '@/components/ui/TaskCardList.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
-import { apiGetTongzhouMenuTree, apiGetTrunkLineMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
 
 
 export default {
@@ -175,13 +175,12 @@ export default {
     },
     async mounted() {
         // 加载菜单和任务数据
-        const [menuData, trunkData, taskData] = await Promise.all([
+        const [menuData, taskData] = await Promise.all([
             apiGetTongzhouMenuTree(),
-            apiGetTrunkLineMenuTree(),
             apiGetTasks({ pageSize: 5 }),
         ]);
         this.menuData = menuData || [];
-        this.trunkLineMenuData = trunkData || [];
+        this.trunkLineMenuData = [];
         this.tableData = taskData?.list || taskData || [];
 
         // 组件挂载时检查路由
@@ -240,6 +239,16 @@ export default {
                 pixelX: pixel ? Math.round(pixel.x / scale) : 950,
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
+            // 干线marker点击时,从菜单数据中匹配对应干线
+            if (this.activeLeftTab === 'trunkLine' && mapData.id && String(mapData.id).startsWith('MOCK-干')) {
+                const matched = this.findTrunkMenuNode(mapData.id);
+                if (matched) {
+                    nodeData.id = matched.id;
+                    nodeData.label = matched.label;
+                    nodeData.intersections = matched.intersections;
+                    nodeData.distances = matched.distances;
+                }
+            }
             console.log(nodeData);
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showCrossingDetailDialogs(nodeData);
@@ -472,6 +481,42 @@ export default {
             console.log('干线菜单点击:', nodeData);
             this.showTrunkLineDalogs(nodeData);
         },
+        findTrunkMenuNode(markerId) {
+            const parts = markerId.split('-');
+            const lineIdx = parseInt(parts[2], 10);
+            const segmentIdx = parseInt(parts[3], 10);
+            const leaves = [];
+            const walk = (nodes) => {
+                if (!Array.isArray(nodes)) return;
+                for (const n of nodes) {
+                    if (n.children && n.children.length > 0) walk(n.children);
+                    else leaves.push(n);
+                }
+            };
+            walk(this.trunkLineMenuData);
+            return leaves.find(n => n._lineIdx === lineIdx && n._segmentIdx === segmentIdx) || null;
+        },
+        handleTrunkMenuUpdate(segments) {
+            this.trunkLineMenuData = [{
+                id: 'trunk_root',
+                label: '主控中心',
+                icon: 'icon-control',
+                isOpen: true,
+                children: [{
+                    id: 'trunk_beijing',
+                    label: '北京市交警总队',
+                    icon: 'icon-police',
+                    isOpen: true,
+                    children: [{
+                        id: 'trunk_tongzhou',
+                        label: '通州区',
+                        icon: 'icon-district',
+                        isOpen: true,
+                        children: segments
+                    }]
+                }]
+            }];
+        },
         async showTrunkLineDalogs(nodeData) {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
             // 优先使用菜单节点自带的路口和距离数据

+ 52 - 5
src/views/TrunkCoordination.vue

@@ -21,7 +21,7 @@
                 @map-crossing-click="handleMapCrossingClick"
                 @map-crossing-mouseover="handleMapCrossingMouseover"
                 @map-crossing-mouseout="handleMapCrossingMouseout"
-
+                @bindTrunkMenuTree="handleTrunkMenuUpdate"
             />
         </template>
 
@@ -113,7 +113,7 @@ import TaskCardList from '@/components/ui/TaskCardList.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
-import { apiGetTongzhouMenuTree, apiGetTrunkLineMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData } from '@/api';
 
 
 export default {
@@ -175,13 +175,12 @@ export default {
     },
     async mounted() {
         // 加载菜单和任务数据
-        const [menuData, trunkData, taskData] = await Promise.all([
+        const [menuData, taskData] = await Promise.all([
             apiGetTongzhouMenuTree(),
-            apiGetTrunkLineMenuTree(),
             apiGetTasks({ pageSize: 5 }),
         ]);
         this.menuData = menuData || [];
-        this.trunkLineMenuData = trunkData || [];
+        this.trunkLineMenuData = [];
         this.tableData = taskData?.list || taskData || [];
 
         // 组件挂载时检查路由
@@ -234,6 +233,16 @@ export default {
                 pixelX: pixel ? Math.round(pixel.x / scale) : 950,
                 pixelY: pixel ? Math.round(pixel.y / scale) : 430,
             }
+            // 干线marker点击时,从菜单数据中匹配对应干线
+            if (this.activeLeftTab === 'trunkLine' && mapData.id && String(mapData.id).startsWith('MOCK-干')) {
+                const matched = this.findTrunkMenuNode(mapData.id);
+                if (matched) {
+                    nodeData.id = matched.id;
+                    nodeData.label = matched.label;
+                    nodeData.intersections = matched.intersections;
+                    nodeData.distances = matched.distances;
+                }
+            }
             console.log(nodeData);
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showCrossingDetailDialogs(nodeData);
@@ -466,6 +475,44 @@ export default {
             console.log('干线菜单点击:', nodeData);
             this.showTrunkLineDalogs(nodeData);
         },
+        findTrunkMenuNode(markerId) {
+            // markerId 格式: MOCK-干-{lineIdx}-{segmentIdx}-{idx}
+            const parts = markerId.split('-');
+            const lineIdx = parseInt(parts[2], 10);
+            const segmentIdx = parseInt(parts[3], 10);
+            // 从菜单叶子节点中找 _lineIdx 和 _segmentIdx 匹配的
+            const leaves = [];
+            const walk = (nodes) => {
+                if (!Array.isArray(nodes)) return;
+                for (const n of nodes) {
+                    if (n.children && n.children.length > 0) walk(n.children);
+                    else leaves.push(n);
+                }
+            };
+            walk(this.trunkLineMenuData);
+            return leaves.find(n => n._lineIdx === lineIdx && n._segmentIdx === segmentIdx) || null;
+        },
+        handleTrunkMenuUpdate(segments) {
+            this.trunkLineMenuData = [{
+                id: 'trunk_root',
+                label: '主控中心',
+                icon: 'icon-control',
+                isOpen: true,
+                children: [{
+                    id: 'trunk_beijing',
+                    label: '北京市交警总队',
+                    icon: 'icon-police',
+                    isOpen: true,
+                    children: [{
+                        id: 'trunk_tongzhou',
+                        label: '通州区',
+                        icon: 'icon-district',
+                        isOpen: true,
+                        children: segments
+                    }]
+                }]
+            }];
+        },
         async showTrunkLineDalogs(nodeData) {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
             // 优先使用菜单节点自带的路口和距离数据