2 Коммиты 5a3ce55b7e ... 9cdd7ed27c

Автор SHA1 Сообщение Дата
  画安 9cdd7ed27c 新增showYSplitLine/showPhaseOffset/showScanLine/scanLineStart等配置参数;mock接口适配新组件roadSegments数据格式 1 неделя назад
  画安 78bf10d61d 重制绿波图组件 1 неделя назад
3 измененных файлов с 318 добавлено и 298 удалено
  1. 2 2
      src/components/ui/CrossingDetailPanel.vue
  2. 303 273
      src/components/ui/TrafficTimeSpace.vue
  3. 13 23
      src/mock/api.js

+ 2 - 2
src/components/ui/CrossingDetailPanel.vue

@@ -590,7 +590,7 @@ export default {
 .signal-timing-wrap {
     flex: 0 0 auto;
     min-height: 0;
-    height: clamp(84px, calc(var(--s) * 149px), 149px);
+    height: clamp(95px, calc(var(--s) * 166px), 166px);
     width: 100%;
     min-width: 0;
     background-color: transparent;
@@ -978,7 +978,7 @@ export default {
 }
 
 .form-editable-area.is-disabled {
-    //opacity: 0.6;
+    opacity: 0.6;
     pointer-events: none;
 }
 

+ 303 - 273
src/components/ui/TrafficTimeSpace.vue

@@ -1,357 +1,386 @@
 <template>
-  <div ref="chartContainer" class="traffic-timespace-chart"></div>
+  <div ref="chartRef" class="traffic-timespace-chart"></div>
 </template>
 
 <script>
 import * as echarts from 'echarts';
-// 1. 引入我们的防变形神器和混入
 import echartsResize, { px2echarts } from '@/mixins/echartsResize.js'; 
 
 export default {
   name: 'TrafficTimeSpace',
   mixins: [echartsResize], 
   props: {
-    intersections: { type: Array, required: true },
-    distances: { type: Array, required: true },
-    waveData: { type: Array, default: () => [] },
-    greenData: { type: Array, default: () => [] },
-    viewWindow: { type: Number, default: 350 },
-    autoScroll: { type: Boolean, default: false },
-    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' },
-    // 【新增】扫描线颜色配置
-    timeLineColor: { type: String, default: '#1a9bff' }
+    // 【终极修复】:使用 0, 50, 0, 0 神级协调相位差
+    roadSegments: { 
+      type: Array, 
+      required: true,
+      default: () => [
+        { name: '交叉口A', distanceNext: 450, offset: 0 },
+        { name: '交叉口B', distanceNext: 669, offset: 50 },
+        { name: '交叉口C', distanceNext: 1050, offset: 0 },
+        { name: '交叉口D', distanceNext: 0, offset: 0 }
+      ]
+    },
+    speedKmh: { type: Number, default: 38.9 },     // 设计车速
+    cycle: { type: Number, default: 100 },         // 红绿灯周期
+    greenDuration: { type: Number, default: 40 },  // 绿灯时长
+    bandwidth: { type: Number, default: 31.5 },    // 波带宽(秒)
+    
+    viewWindow: { type: Number, default: 420 },    
+    autoScroll: { type: Boolean, default: false }, 
+    scrollSpeed: { type: Number, default: 0.2 },   
+    
+    upWaveColor: { type: String, default: 'rgba(46, 204, 113, 0.45)' },
+    downWaveColor: { type: String, default: 'rgba(52, 152, 219, 0.45)' },
+    showYSplitLine: { type: Boolean, default: false },
+    showPhaseOffset: { type: Boolean, default: true },
+    showScanLine: { type: Boolean, default: true },
+    scanLineColor: { type: String, default: 'rgba(255, 60, 60, 0.8)' },
+    scanLineStart: { type: Number, default: 0 }
   },
   data() {
     return {
+      $_chart: null,
       scrollTimer: null,
-      currentViewTime: 0,
-      timeLineTimer: null,
-      currentTimeIndicator: Math.random() * this.viewWindow,
-      // 每条绿波带的速度(初始固定,扫描线经过后定格)
-      waveSpeeds: [],
-      // 当前扫描线命中的所有绿波带索引
-      activeWaveIndices: []
+      scanLineTimer: null,
+      currentViewMinY: 0,
+      currentScanX: 0,
+      barWidth: 8,
+      gap: 2,
     };
   },
   computed: {
-    maxDistance() { return Math.max(...this.distances, 100); },
-    maxDataTime() {
-      let max = 0;
-      this.waveData.forEach(w => max = Math.max(max, w.xBR, w.xTR));
-      this.greenData.forEach(g => max = Math.max(max, g.end));
-      return max;
-    },
-    echartsWaveData() {
-      return this.waveData.map((w, i) => {
-        const spd = this.waveSpeeds[i] || 50;
-        return [w.yBottom, w.yTop, w.xBL, w.xBR, w.xTL, w.xTR, spd + 'km/h', w.direction || 'up'];
+    intersections() {
+      let currentX = 0;
+      return this.roadSegments.map((seg, i) => {
+        const intersection = {
+          id: `I${i}`,
+          name: seg.name,
+          distanceNext: seg.distanceNext,
+          x: currentX,
+          offsetText: `${seg.offset}秒`,
+          offset: seg.offset
+        };
+        currentX += seg.distanceNext;
+        return intersection;
       });
     },
-    echartsGreenData() { return this.greenData.map(g => [g.y, g.start, g.end]); },
-    echartsRedData() { return this.distances.map(y => [y]); },
-    reversedIntersections() { return [...this.intersections].reverse(); }
+    maxX() {
+      const last = this.intersections[this.intersections.length - 1];
+      return last.x + 50;
+    },
+    maxDataTime() {
+      return this.viewWindow * 3; 
+    }
   },
   mounted() {
     this.$nextTick(() => {
       this.initChart();
       if (this.autoScroll) this.startScroll();
-      // 【新增】组件挂载后启动扫描线
-      this.startTimeLine();
+      if (this.showScanLine) this.startScanLine();
     });
   },
   beforeDestroy() {
     this.stopScroll();
-    // 【新增】销毁时清理定时器
-    this.stopTimeLine(); 
+    this.stopScanLine();
+    if (this.$_chart) this.$_chart.dispose();
   },
   watch: {
-    waveData() {
-      this.initWaveSpeeds();
-      this.updateChart();
-    },
-    greenData() { this.updateChart(); },
+    roadSegments: { handler() { this.updateChart(); }, deep: true },
+    speedKmh() { this.updateChart(); },
     autoScroll(val) { val ? this.startScroll() : this.stopScroll(); }
   },
   methods: {
-    // 为每条绿波带生成初始固定速度
-    initWaveSpeeds() {
-      this.waveSpeeds = this.waveData.map(w => w.speed || (45 + Math.round(Math.random() * 10)));
-    },
-
     initChart() {
-      this.initWaveSpeeds();
-      this.$_chart = echarts.init(this.$refs.chartContainer);
+      if (!this.$refs.chartRef) return;
+      this.$_chart = echarts.init(this.$refs.chartRef);
       this.updateChart();
     },
 
+    generateBarData() {
+      const data = [];
+      const colors = { red: '#e74c3c', green: '#2ecc71', blue: '#3498db' };
+      
+      this.intersections.forEach(intersection => {
+        for (let k = -2; k <= Math.ceil(this.maxDataTime / this.cycle) + 2; k++) {
+          const pStart = intersection.offset + k * this.cycle;
+          const pEnd = pStart + this.greenDuration;
+          const rStart = pEnd;
+          const rEnd = pStart + this.cycle;
+          
+          data.push([intersection.x, pStart, pEnd, colors.green, -1]); 
+          data.push([intersection.x, pStart, pEnd, colors.blue, 1]);   
+          data.push([intersection.x, rStart, rEnd, colors.red, -1]);   
+          data.push([intersection.x, rStart, rEnd, colors.red, 1]);    
+        }
+      });
+      return data;
+    },
+
+    getWaveData() {
+      const speedMs = this.speedKmh / 3.6; // 换算为 m/s
+      const startX = this.intersections[0].x;
+      const endX = this.intersections[this.intersections.length - 1].x;
+      const travelTime = (endX - startX) / speedMs;
+
+      // 【终极微调】:发车时间精确控制
+      // 正向绿波 (A -> D):10秒起步,刚好完美贴合所有绿灯边缘
+      const gStartY = 10; 
+      const greenBottomLine = [
+        [startX, gStartY],
+        [endX, gStartY + travelTime]
+      ];
+      const greenTopLine = [
+        [startX, gStartY + this.bandwidth],
+        [endX, gStartY + travelTime + this.bandwidth]
+      ];
+      const greenCoords = [...greenBottomLine, ...[...greenTopLine].reverse()];
+
+      // 反向蓝波 (D -> A):100秒起步,完美避开 B 路口的红灯区域
+      const bStartYAtD = 100; 
+      const blueBottomLine = [
+        [endX, bStartYAtD],
+        [startX, bStartYAtD + travelTime]
+      ];
+      const blueTopLine = [
+        [endX, bStartYAtD + this.bandwidth],
+        [startX, bStartYAtD + travelTime + this.bandwidth]
+      ];
+      const blueCoords = [...blueBottomLine, ...[...blueTopLine].reverse()];
+
+      return [
+        { 
+          coords: greenCoords, 
+          bottomLine: greenBottomLine, 
+          topLine: greenTopLine,
+          color: this.upWaveColor, 
+          lineCol: '#2ecc71', 
+          isBlue: false 
+        },
+        { 
+          coords: blueCoords, 
+          bottomLine: blueBottomLine, 
+          topLine: blueTopLine,
+          color: this.downWaveColor, 
+          lineCol: '#3498db', 
+          isBlue: true 
+        }
+      ];
+    },
+
     updateChart() {
       if (!this.$_chart) return;
 
-      const self = this;
-      const distances = this.distances;
-      const intersections = this.reversedIntersections;
-      const maxDist = this.maxDistance;
-      const step = distances.length > 1 ? distances[1] - distances[0] : 300;
-
-      this.$_chart.setOption({
+      const option = {
         backgroundColor: 'transparent',
         animation: false,
         tooltip: { show: false },
-        grid: {
-          left: px2echarts(80),
-          right: px2echarts(15),
-          top: px2echarts(10),
-          bottom: px2echarts(25)
-        },
+        grid: { left: 50, right: 100, top: 40, bottom: 80 },
         xAxis: {
-          type: 'value',
-          min: this.currentViewTime,
-          max: this.currentViewTime + this.viewWindow,
-          axisLabel: {
-            color: '#7b95b9',
-            formatter: '{value}s',
-            fontSize: px2echarts(10)
-          },
-          splitLine: { show: true, lineStyle: { color: '#1a305d', type: 'solid' } },
-          axisLine: { lineStyle: { color: '#31548e' } }
+          type: 'value', min: 0, max: this.maxX,
+          axisLine: { show: true, lineStyle: { color: 'rgba(255,255,255,0.15)' } },
+          axisTick: { show: false },
+          axisLabel: { show: false },
+          splitLine: { show: false }
         },
         yAxis: {
-          type: 'value',
-          min: 0,
-          max: maxDist,
-          interval: step,
-          axisLabel: {
-            interval: 0,
-            color: '#9cb1d4',
-            fontWeight: 'bold',
-            fontSize: px2echarts(10),
-            formatter: value => distances.includes(value) ? intersections[distances.indexOf(value)] : ''
-          },
-          splitLine: { show: true, lineStyle: { color: '#1a305d' } }
+          type: 'value', min: this.currentViewMinY, max: this.currentViewMinY + this.viewWindow,
+          interval: 20, name: '时间 (秒)',
+          nameTextStyle: { color: '#a0aabf', padding: [0, 30, 0, 0] },
+          axisLabel: { color: '#a0aabf', fontSize: 10 },
+          splitLine: { show: this.showYSplitLine, lineStyle: { type: 'dashed', color: 'rgba(255, 255, 255, 0.08)' } }
         },
-        // 【修改】给每个系列加上 id,新增时间线系列
         series: [
           {
             id: 'waveSeries',
             type: 'custom',
-            renderItem: function (params, api) { return self.renderWave(params, api); },
-            data: this.echartsWaveData,
             clip: true,
-            z: 1
+            data: this.getWaveData(),
+            renderItem: (params, api) => {
+              const item = this.getWaveData()[params.dataIndex];
+              const pixelOffsetX = item.isBlue ? (this.barWidth * 1.5 + this.gap) : (this.barWidth / 2);
+              
+              const mappedCoords = item.coords.map(c => {
+                const pt = api.coord(c);
+                return [pt[0] + pixelOffsetX, pt[1]];
+              });
+
+              const mappedBottom = item.bottomLine.map(c => {
+                const pt = api.coord(c);
+                return [pt[0] + pixelOffsetX, pt[1]];
+              });
+              const mappedTop = item.topLine.map(c => {
+                const pt = api.coord(c);
+                return [pt[0] + pixelOffsetX, pt[1]];
+              });
+
+              const segIdx = 0; 
+              const pB1 = mappedBottom[segIdx];
+              const pB2 = mappedBottom[segIdx + 1];
+              const pT1 = mappedTop[segIdx];
+              const pT2 = mappedTop[segIdx + 1];
+
+              let angle = Math.atan2(pB2[1] - pB1[1], pB2[0] - pB1[0]);
+              if (item.isBlue) {
+                angle -= Math.PI;
+              }
+
+              const percent = 0.15; 
+              const midBottomX = pB1[0] + (pB2[0] - pB1[0]) * percent;
+              const midBottomY = pB1[1] + (pB2[1] - pB1[1]) * percent;
+              const midTopX = pT1[0] + (pT2[0] - pT1[0]) * percent;
+              const midTopY = pT1[1] + (pT2[1] - pT1[1]) * percent;
+
+              return {
+                type: 'group',
+                children: [
+                  { type: 'polygon', shape: { points: mappedCoords }, style: { fill: item.color } },
+                  ...(() => {
+                    // 虚线与绿波带之间留间隙,沿垂直于底线方向向外偏移
+                    const lineGap = 4;
+                    const bDx = mappedBottom[1][0] - mappedBottom[0][0];
+                    const bDy = mappedBottom[1][1] - mappedBottom[0][1];
+                    const bLen = Math.sqrt(bDx * bDx + bDy * bDy) || 1;
+                    const perpX = bDy / bLen;
+                    const perpY = -bDx / bLen;
+                    const midBx = (mappedBottom[0][0] + mappedBottom[1][0]) / 2;
+                    const midBy = (mappedBottom[0][1] + mappedBottom[1][1]) / 2;
+                    const midTx = (mappedTop[0][0] + mappedTop[1][0]) / 2;
+                    const midTy = (mappedTop[0][1] + mappedTop[1][1]) / 2;
+                    const dot = (midTx - midBx) * perpX + (midTy - midBy) * perpY;
+                    const sign = dot > 0 ? -1 : 1;
+                    const offsetBottom = mappedBottom.map(p => [p[0] + sign * perpX * lineGap, p[1] + sign * perpY * lineGap]);
+                    return [
+                      { type: 'polyline', shape: { points: offsetBottom }, style: { stroke: item.lineCol, lineDash: [4, 4], lineWidth: 1 } }
+                    ];
+                  })(),
+                  { type: 'text', x: midBottomX, y: midBottomY + 12, rotation: angle, style: { text: `${this.speedKmh}km/h`, fill: '#fff', fontSize: 11, textAlign: 'center' } },
+                  ...(() => {
+                    const cxBw = (midBottomX + midTopX) / 2;
+                    const cyBw = (midBottomY + midTopY) / 2;
+                    const dxBw = midTopX - midBottomX;
+                    const dyBw = midTopY - midBottomY;
+                    const lenBw = Math.sqrt(dxBw * dxBw + dyBw * dyBw) || 1;
+                    const uxBw = dxBw / lenBw;
+                    const uyBw = dyBw / lenBw;
+                    const gapBw = 10;
+                    return [
+                      { type: 'line', shape: { x1: midBottomX, y1: midBottomY, x2: cxBw - uxBw * gapBw, y2: cyBw - uyBw * gapBw }, style: { stroke: 'rgba(255, 255, 255, 0.4)' } },
+                      { type: 'text', x: cxBw, y: cyBw, rotation: angle, style: { text: `${this.bandwidth}s`, fill: '#fff', fontSize: 11, textAlign: 'center', textVerticalAlign: 'middle' } },
+                      { type: 'line', shape: { x1: cxBw + uxBw * gapBw, y1: cyBw + uyBw * gapBw, x2: midTopX, y2: midTopY }, style: { stroke: 'rgba(255, 255, 255, 0.4)' } }
+                    ];
+                  })()
+                ]
+              };
+            }
           },
           {
-            id: 'redSeries',
+            id: 'lightSeries',
             type: 'custom',
-            renderItem: function (params, api) { return self.renderRedBackground(params, api); },
-            data: this.echartsRedData,
             clip: true,
-            z: 2
+            data: this.generateBarData(),
+            renderItem: (params, api) => {
+              const xValue = api.value(0);
+              const yStart = api.value(1);
+              const yEnd = api.value(2);
+              const color = api.value(3);
+              const direction = api.value(4);
+
+              const startP = api.coord([xValue, yStart]);
+              const endP = api.coord([xValue, yEnd]);
+              const rectX = direction === -1 ? startP[0] : (startP[0] + this.barWidth + this.gap);
+
+              return {
+                type: 'rect',
+                shape: { x: rectX, y: endP[1], width: this.barWidth, height: startP[1] - endP[1] },
+                style: { fill: color }
+              };
+            }
           },
           {
-            id: 'greenSeries',
+            id: 'axisLabels',
             type: 'custom',
-            renderItem: function (params, api) { return self.renderGreenLight(params, api); },
-            data: this.echartsGreenData,
-            clip: true,
-            z: 3
+            clip: false, 
+            data: this.intersections,
+            renderItem: (params, api) => {
+              const intersection = this.intersections[params.dataIndex];
+              const point = api.coord([intersection.x, this.currentViewMinY]); 
+              const centerXPx = point[0] + this.barWidth + this.gap / 2;
+              
+              const children = [
+                { type: 'text', x: centerXPx, y: point[1] + 8, style: { text: intersection.name, fill: '#fff', textAlign: 'center', fontSize: 12 } }
+              ];
+              if (this.showPhaseOffset) {
+                children.push({ type: 'text', x: centerXPx, y: point[1] + 22, style: { text: `相位差: ${intersection.offsetText}`, fill: '#a0aabf', textAlign: 'center', fontSize: 11 } });
+              }
+
+              if (intersection.distanceNext) {
+                const nextP = api.coord([intersection.x + intersection.distanceNext, this.currentViewMinY]);
+                const nextCenterXPx = nextP[0] + this.barWidth + this.gap / 2;
+                const midXPx = (centerXPx + nextCenterXPx) / 2;
+                
+                children.push({ type: 'line', shape: { x1: centerXPx + 30, y1: point[1] + 13, x2: nextCenterXPx - 30, y2: point[1] + 13 }, style: { stroke: 'rgba(255,255,255,0.1)', lineDash: [2, 2] } });
+                children.push({ type: 'text', x: midXPx, y: point[1] + 13, style: { text: `${intersection.distanceNext}m`, fill: '#a0aabf', textAlign: 'center', fontSize: 11 } });
+              }
+              return { type: 'group', children };
+            }
           },
-          {
-            id: 'timeLineSeries', // 【新增】垂直扫描线系列
+          ...(this.showScanLine ? [{
+            id: 'scanLineSeries',
             type: 'custom',
-            renderItem: function (params, api) { return self.renderTimeLine(params, api); },
-            data: [[this.currentTimeIndicator]], // 初始数据
             clip: true,
-            z: 10 // 放在最上层
-          }
-        ]
-      });
-    },
-
-    renderRedBackground(params, api) {
-      const y = api.value(0);
-      const startX = api.coord([0, y])[0];
-      const endX = api.coord([this.maxDataTime || this.viewWindow, y])[0];
-      return {
-        type: 'rect',
-        shape: {
-          x: startX,
-          y: api.coord([0, y])[1] - px2echarts(2),
-          width: endX - startX,
-          height: px2echarts(4)
-        },
-        style: { fill: '#f02828' }
-      };
-    },
-
-    renderGreenLight(params, api) {
-      const y = api.value(0);
-      const p1 = api.coord([api.value(1), y]);
-      const p2 = api.coord([api.value(2), y]);
-      return {
-        type: 'rect',
-        shape: {
-          x: p1[0],
-          y: p1[1] - px2echarts(3),
-          width: p2[0] - p1[0],
-          height: px2echarts(6)
-        },
-        style: api.style({ fill: '#68e75f' })
-      };
-    },
-
-    renderWave(params, api) {
-      const yBottom = api.value(0), yTop = api.value(1);
-      const xBL = api.value(2), xBR = api.value(3), xTL = api.value(4), xTR = api.value(5);
-      const text = api.value(6), dir = api.value(7);
-
-      const ptBL = api.coord([xBL, yBottom]), ptBR = api.coord([xBR, yBottom]);
-      const ptTL = api.coord([xTL, yTop]), ptTR = api.coord([xTR, yTop]);
-      const angle = -Math.atan2(ptTL[1] - ptBL[1], ptTL[0] - ptBL[0]);
-      const fillColor = dir === 'up' ? this.upWaveColor : this.downWaveColor;
-
-      return {
-        type: 'group',
-        children: [
-          {
-            type: 'polygon',
-            shape: { points: [ptBL, ptBR, ptTR, ptTL] },
-            z2: 1,
-            style: api.style({ fill: fillColor, stroke: 'none' })
-          },
-          {
-            type: 'text',
-            x: (ptBL[0] + ptTR[0]) / 2,
-            y: (ptBL[1] + ptTR[1]) / 2,
-            rotation: angle,
-            z2: 5,
-            style: {
-              text: text,
-              fill: this.waveLabelColor,
-              font: `bold ${px2echarts(12)}px sans-serif`,
-              textAlign: 'center',
-              textVerticalAlign: 'middle'
-            }
-          }
+            data: [[this.currentScanX]],
+            renderItem: (params, api) => {
+              const xDist = api.value(0);
+              const top = api.coord([xDist, this.currentViewMinY + this.viewWindow]);
+              const bottom = api.coord([xDist, this.currentViewMinY]);
+              return {
+                type: 'line',
+                shape: { x1: top[0], y1: top[1], x2: bottom[0], y2: bottom[1] },
+                style: { stroke: this.scanLineColor, lineWidth: 2 }
+              };
+            },
+            z: 10
+          }] : [])
         ]
       };
+      this.$_chart.setOption(option, { replaceMerge: ['series'] });
     },
 
-    // 【新增】渲染垂直扫描线
-    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(5), // 线条粗细
-          lineDash: null // 实线
+    startScanLine() {
+      this.stopScanLine();
+      this.currentScanX = this.maxX * Math.min(1, Math.max(0, this.scanLineStart));
+      const step = this.maxX / 100;
+      this.scanLineTimer = setInterval(() => {
+        this.currentScanX += step;
+        if (this.currentScanX > this.maxX) {
+          this.currentScanX = 0;
         }
-      };
-    },
-
-    // 启动扫描线动画(在 viewWindow 内循环,经过绿波带时速度缓慢波动)
-    startTimeLine() {
-      this.stopTimeLine();
-      let lastTime = Date.now();
-      let elapsed = 0;
-      // 控制速度变化节奏:每隔一段时间才变一次
-      let speedChangeTimer = 0;
-
-      this.timeLineTimer = setInterval(() => {
-        const now = Date.now();
-        const delta = (now - lastTime) / 1000;
-        lastTime = now;
-        elapsed += delta;
-        speedChangeTimer += delta;
-
-        this.currentTimeIndicator += 1;
-
-        // 在 viewWindow 范围内循环,循环时重置所有速度
-        if (this.currentTimeIndicator > this.viewWindow) {
-          this.currentTimeIndicator = 0;
-          this.initWaveSpeeds();
-        }
-
-        // 检测扫描线命中的所有绿波带(支持重叠)
-        const x = this.currentTimeIndicator;
-        const hitIndices = [];
-        for (let i = 0; i < this.waveData.length; i++) {
-          const w = this.waveData[i];
-          const xMin = Math.min(w.xBL, w.xTL);
-          const xMax = Math.max(w.xBR, w.xTR);
-          if (x >= xMin && x <= xMax) {
-            hitIndices.push(i);
-          }
-        }
-
-        const prevIndices = this.activeWaveIndices;
-        this.activeWaveIndices = hitIndices;
-
-        // 扫描线在绿波带内时,每0.8秒缓慢变化所有命中带的速度
-        let needFullUpdate = false;
-        if (hitIndices.length > 0 && speedChangeTimer >= 0.8) {
-          speedChangeTimer = 0;
-          hitIndices.forEach(idx => {
-            const cur = this.waveSpeeds[idx] || 50;
-            const change = Math.round((Math.random() - 0.5) * 4);
-            this.$set(this.waveSpeeds, idx, Math.max(45, Math.min(55, cur + change)));
-          });
-          needFullUpdate = true;
-        }
-
-        // 命中集合变化时也需要刷新
-        if (hitIndices.length !== prevIndices.length ||
-            hitIndices.some((v, i) => v !== prevIndices[i])) {
-          needFullUpdate = true;
-        }
-
         if (this.$_chart) {
-          if (needFullUpdate) {
-            this.$_chart.setOption({
-              series: [
-                { id: 'waveSeries', data: this.echartsWaveData },
-                { id: 'timeLineSeries', data: [[this.currentTimeIndicator]] }
-              ]
-            });
-          } else {
-            this.$_chart.setOption({
-              series: [{ id: 'timeLineSeries', data: [[this.currentTimeIndicator]] }]
-            });
-          }
+          this.$_chart.setOption({
+            series: [{ id: 'scanLineSeries', data: [[this.currentScanX]] }]
+          });
         }
       }, 1000);
     },
 
-    // 【新增】停止扫描线动画
-    stopTimeLine() {
-      if (this.timeLineTimer) {
-        clearInterval(this.timeLineTimer);
-        this.timeLineTimer = null;
+    stopScanLine() {
+      if (this.scanLineTimer) {
+        clearInterval(this.scanLineTimer);
+        this.scanLineTimer = null;
       }
     },
 
     startScroll() {
       this.stopScroll();
       this.scrollTimer = setInterval(() => {
-        this.currentViewTime += this.scrollSpeed;
-        if (this.currentViewTime > this.maxDataTime - this.viewWindow) {
-          this.currentViewTime = 0;
+        this.currentViewMinY += this.scrollSpeed;
+        if (this.currentViewMinY > this.maxDataTime - this.viewWindow) {
+          this.currentViewMinY = 0; 
         }
         if (this.$_chart) {
           this.$_chart.setOption({
-            xAxis: { min: this.currentViewTime, max: this.currentViewTime + this.viewWindow }
+            yAxis: { min: this.currentViewMinY, max: this.currentViewMinY + this.viewWindow }
           });
         }
       }, 16);
@@ -369,8 +398,9 @@ export default {
 
 <style scoped>
 .traffic-timespace-chart {
-  flex: 1;
   width: 100%;
+  height: 100%;
   min-height: 0;
+  flex: 1;
 }
 </style>

+ 13 - 23
src/mock/api.js

@@ -645,29 +645,19 @@ export async function apiGetKeyIntersections() {
 
 export async function apiGetTrafficTimeSpace(opts = {}) {
   await delay(300)
-  const { speed = 15, cycle = 120, band = 40, totalTime = 1800 } = opts
-  const intersections = opts.intersections || DB.timeSpaceData.intersections
-  const rawDistances = opts.distances || DB.timeSpaceData.distances
-  // 将不均匀的物理距离归一化为等间距,保证绿波带视觉对齐
-  const step = 500
-  const distances = rawDistances.map((_, i) => i * step)
-  const maxDist = distances[distances.length - 1]
-  const waveData = [], greenData = []
-
-  for (let t = 0; t <= totalTime; t += cycle) {
-    const upKmh = 45 + Math.random() * 10   // 45-55 km/h 随机
-    const downKmh = 45 + Math.random() * 10
-    const upSpd = upKmh / 3.6               // 转 m/s
-    const downSpd = downKmh / 3.6
-    const ds = t + cycle / 2
-    waveData.push({ yBottom: 0, yTop: maxDist, xBL: t, xBR: t + band, xTL: t + maxDist / upSpd, xTR: t + maxDist / upSpd + band, label: Math.round(upKmh) + 'km/h', direction: 'up', speed: Math.round(upKmh) })
-    waveData.push({ yBottom: maxDist, yTop: 0, xBL: ds, xBR: ds + band, xTL: ds + maxDist / downSpd, xTR: ds + maxDist / downSpd + band, label: Math.round(downKmh) + 'km/h', direction: 'down', speed: Math.round(downKmh) })
-    distances.forEach(y => {
-      greenData.push({ y, start: t + y / upSpd, end: t + y / upSpd + band })
-      greenData.push({ y, start: ds + (maxDist - y) / downSpd, end: ds + (maxDist - y) / downSpd + band })
-    })
-  }
-  return ok({ intersections, distances, waveData, greenData })
+  return ok({
+    roadSegments: [
+      { name: '交叉口A', distanceNext: 450, offset: 0 },
+      { name: '交叉口B', distanceNext: 669, offset: 50 },
+      { name: '交叉口C', distanceNext: 1050, offset: 0 },
+      { name: '交叉口D', distanceNext: 0, offset: 0 }
+    ],
+    speedKmh: 38.9,
+    cycle: 100,
+    greenDuration: 40,
+    bandwidth: 31.5,
+    scanLineStart: Math.round(Math.random() * 100) / 100
+  })
 }
 
 // ═══════════════════════════════════════════════════════════════════════