Kaynağa Gözat

总览小弹窗(CrossingPanel)相位图升级: 双图模式 + 生长条扫描线 + 自适应缩放

  - CrossingPanel.vue: 新增 isDual 双图布局(本周期实时 clipToActive + compactScanLine / 上周期静态), ResizeObserver 动态
   --s 缩放, 容器高度改 clamp 自适应
  - mock/api.js: apiGetCrossingPanelData 返回 thisCycle/lastCycle 双图数据
  - StatusMonitoring/SpecialSituationMonitoring/TrunkCoordination: 弹窗高度 260→340 适配双图
画安 2 hafta önce
ebeveyn
işleme
83a0045e2c

+ 111 - 20
src/components/ui/CrossingPanel.vue

@@ -3,8 +3,34 @@
         <div class="intersection-video-wrap">
             <IntersectionMapVideos :mapData="intersectionData" />
         </div>
-        <div class="signal-timing-wrap">
-            <SignalTimingChart :cycleLength="cycleLength" :currentTime="currentSec" :phaseData="mockPhaseData" :showScanLine="dataReady" :autoScan="dataReady" @scan-tick="onScanTick" />
+        <div class="signal-timing-wrap" :class="{ 'is-dual': isDual }">
+            <template v-if="isDual">
+                <div class="timing-row timing-row-live">
+                    <div class="row-label">
+                        本周期 实时<span v-if="thisCycle"> · {{ thisCycle.schemeName }}</span>
+                    </div>
+                    <div class="row-chart">
+                        <SignalTimingChart :cycleLength="thisCycle.cycleLength" :currentTime="currentSec"
+                            :phaseData="thisCycle.phaseData" :showScanLine="dataReady" :showScanLineLabel="dataReady"
+                            :clipToActive="true" :compactScanLine="true" :autoScan="dataReady"
+                            @scan-tick="onScanTick" />
+                    </div>
+                </div>
+                <div class="timing-row timing-row-last">
+                    <div class="row-label">
+                        上周期 方案
+                        <span v-if="lastCycle"> · 实际 {{ lastCycle.actualDuration }}s / 计划 {{ lastCycle.cycleLength }}s</span>
+                    </div>
+                    <div class="row-chart">
+                        <SignalTimingChart v-if="lastCycle" :cycleLength="lastCycle.cycleLength" :currentTime="0"
+                            :phaseData="lastCycle.phaseData" :showScanLine="false" :showScanLineLabel="false" />
+                        <div v-else class="empty-placeholder">暂无上周期数据</div>
+                    </div>
+                </div>
+            </template>
+            <SignalTimingChart v-else :cycleLength="cycleLength" :currentTime="currentSec"
+                :phaseData="mockPhaseData" :showScanLine="dataReady" :autoScan="dataReady"
+                @scan-tick="onScanTick" />
         </div>
     </div>
 </template>
@@ -31,23 +57,48 @@ export default {
             intersectionData: {},
             currentRoute: {},
             cycleLength: 140,
-            currentSec: 15,
-            mockPhaseData: []
+            currentSec: 0,
+            mockPhaseData: [],
+            thisCycle: null,
+            lastCycle: null,
         }
     },
-    async mounted() {
-        const nodeId = this.$attrs.id || this.id;
-        const data = await apiGetCrossingPanelData(nodeId);
-        if (data) {
-            this.currentRoute = data.currentRoute || {};
-            this.intersectionData = data.intersectionData || {};
-            this.mockPhaseData = data.phaseData || [];
-            if (data.cycleLength) this.cycleLength = data.cycleLength;
-            if (data.currentTime !== undefined) this.currentSec = data.currentTime;
-            this.$nextTick(() => { this.dataReady = true; });
-        }
+    computed: {
+        isDual() {
+            return !!(this.thisCycle && this.thisCycle.phaseData && this.thisCycle.phaseData.length);
+        },
+    },
+    mounted() {
+        this.initScaleObserver();
+        this.loadData();
+    },
+    beforeDestroy() {
+        if (this._ro) this._ro.disconnect();
     },
     methods: {
+        initScaleObserver() {
+            const ro = new ResizeObserver(entries => {
+                const { width } = entries[0].contentRect;
+                const s = Math.min(width / 260, 1);
+                this.$el.style.setProperty('--s', s);
+            });
+            ro.observe(this.$el);
+            this._ro = ro;
+        },
+        async loadData() {
+            const nodeId = this.$attrs.id || this.id;
+            const data = await apiGetCrossingPanelData(nodeId);
+            if (data) {
+                this.currentRoute = data.currentRoute || {};
+                this.intersectionData = data.intersectionData || {};
+                this.mockPhaseData = data.phaseData || [];
+                if (data.cycleLength) this.cycleLength = data.cycleLength;
+                if (data.currentTime !== undefined) this.currentSec = data.currentTime;
+                this.thisCycle = data.thisCycle || null;
+                this.lastCycle = data.lastCycle || null;
+                this.$nextTick(() => { this.dataReady = true; });
+            }
+        },
         onScanTick(activeTime) {
             if (!this.mockPhaseData || this.mockPhaseData.length === 0) return;
             const phase = this.mockPhaseData.find(p => p[0] === 0 && activeTime >= p[1] && activeTime < p[2]);
@@ -98,10 +149,11 @@ export default {
 
 <style scoped>
 .crossing-panel {
+    --s: 1;
     display: flex;
     flex-direction: column;
     width: 100%;
-    height: 100%; 
+    height: 100%;
     min-height: 0;
 }
 
@@ -112,11 +164,10 @@ export default {
 }
 
 .signal-timing-wrap {
-    flex: 1; 
+    flex: 0 0 auto;
     min-height: 0;
-    --s: 1;
+    height: clamp(56px, calc(var(--s) * 80px), 80px);
     width: 100%;
-    height: 80px;
     min-width: 0;
     background-color: transparent;
     box-sizing: border-box;
@@ -124,7 +175,47 @@ export default {
     display: flex;
     flex-direction: column;
     overflow: hidden;
-    padding: calc(var(--s) * 10px) 0 0 0;
+    padding: clamp(3px, calc(var(--s) * 10px), 10px) 0 0 0;
+}
+
+.signal-timing-wrap.is-dual {
+    height: clamp(90px, calc(var(--s) * 140px), 140px);
+}
+
+.timing-row {
+    flex: 1 1 0;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+}
+
+.row-label {
+    flex: 0 0 auto;
+    font-size: clamp(8px, calc(var(--s) * 11px), 11px);
+    color: #9ca3af;
+    padding: 0 4px;
+    line-height: 1;
+    margin-bottom: 2px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.row-chart {
+    flex: 1 1 0;
+    min-height: 0;
+    display: flex;
+    position: relative;
+    overflow: hidden;
+}
+
+.empty-placeholder {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #6b7280;
+    font-size: clamp(10px, calc(var(--s) * 12px), 12px);
 }
 
 .header {

+ 20 - 0
src/mock/api.js

@@ -1064,6 +1064,25 @@ export async function apiGetCrossingPanelData(id) {
   const phaseData = _makePhaseData(cycleLength, false, 'simple', id)
   const currentTime = Math.floor(Date.now() / 1000) % cycleLength
 
+  const _nowSec = Math.floor(Date.now() / 1000)
+  const thisCycle = {
+    schemeId: 'sys_a',
+    schemeName: '默认配时方案',
+    cycleLength,
+    currentTime,
+    phaseData,
+    phaseDiff: (seed * 7) % 25,
+    coordTime: (seed * 13) % 60,
+  }
+  const lastCycle = {
+    schemeId: 'sys_a',
+    schemeName: '默认配时方案',
+    cycleLength,
+    actualDuration: cycleLength + 2,
+    endedAt: new Date((_nowSec - currentTime) * 1000).toISOString(),
+    phaseData,
+  }
+
   return ok({
     currentRoute: {
       id, name: point ? point.name : id,
@@ -1074,6 +1093,7 @@ export async function apiGetCrossingPanelData(id) {
     intersectionData: config,
     phaseData,
     cycleLength, currentTime,
+    thisCycle, lastCycle,
   })
 }
 

+ 1 - 1
src/views/SpecialSituationMonitoring.vue

@@ -376,7 +376,7 @@ export default {
                 title: nodeData.label,
                 component: 'CrossingPanel',
                 width: 260,
-                height: 260,
+                height: 340,
                 center: false,
                 showClose: true,
                 position: { x: (nodeData.pixelX || 950) + 20, y: nodeData.pixelY || 430 },

+ 1 - 1
src/views/StatusMonitoring.vue

@@ -502,7 +502,7 @@ export default {
                 title: nodeData.label,
                 component: 'CrossingPanel',
                 width: 260,
-                height: 260,
+                height: 340,
                 center: false,
                 showClose: true,
                 position: { x: (nodeData.pixelX || 950) + 10, y: nodeData.pixelY || 430 },

+ 1 - 1
src/views/TrunkCoordination.vue

@@ -380,7 +380,7 @@ export default {
                 title: nodeData.label,
                 component: 'CrossingPanel',
                 width: 260,
-                height: 260,
+                height: 340,
                 center: false,
                 showClose: true,
                 position: { x: (nodeData.pixelX || 950) + 20, y: nodeData.pixelY || 430 },