Parcourir la source

调整交通路口信号监控和信号灯配时图组合组件的布局

画安 il y a 2 mois
Parent
commit
2e1ccdb609

BIN
src/assets/videos/video1.mp4


BIN
src/assets/videos/video2.mp4


+ 60 - 73
src/components/IntersectionMap.vue

@@ -1,5 +1,7 @@
 <template>
-    <div ref="konvaContainer" class="intersection-map-container"></div>
+  <div class="map-wrapper" ref="wrapper">
+    <div class="konva-container" ref="konvaContainer"></div>
+  </div>
 </template>
 
 <script>
@@ -8,7 +10,6 @@ import Konva from 'konva';
 export default {
   name: 'IntersectionMap',
   props: {
-    // 接收父组件传来的路口数据
     mapData: {
       type: Object,
       required: true
@@ -18,17 +19,16 @@ export default {
     return {
       stage: null,
       layer: null,
-      armsNodes: {}, // 存储四个方向的道路实例
-      panelNodes: {}, // 存储中央面板的文字实例
-      // 颜色配置
+      armsNodes: {}, 
+      panelNodes: {}, 
+      resizeObserver: null, 
       C: {
         BG: '#212842', ROAD: '#3d3938', YELLOW: '#D9A73D', WHITE: '#E0E0E0',
         SIGNAL_RED: '#FF5252', SIGNAL_GREEN: '#8DF582',
         PANEL_BG: 'rgba(30, 30, 40, 0.85)', BLUE: '#448AFF'
       },
-      // 尺寸配置
       sizeConfig: {
-        stageSize: 900,
+        stageSize: 900, 
         laneWidth: 40,
         halfRoad: 160,
         roadWidth: 320,
@@ -42,89 +42,44 @@ export default {
       this.renderStaticConfig();
       this.updateDynamicSignals();
     }
-    // 监听容器尺寸变化,自适应缩放
-    this._roaPending = false;
-    this._resizeObserver = new ResizeObserver(() => {
-      if (!this._roaPending) {
-        this._roaPending = true;
-        requestAnimationFrame(() => {
-          this._roaPending = false;
-          this.fitToContainer();
-        });
-      }
-    });
-    this._resizeObserver.observe(this.$refs.konvaContainer);
+    this.initResizeObserver();
   },
   beforeDestroy() {
-    if (this._resizeObserver) {
-      this._resizeObserver.disconnect();
+    if (this.resizeObserver) {
+      this.resizeObserver.disconnect();
     }
     if (this.stage) {
       this.stage.destroy();
     }
   },
   watch: {
-    // 监听数据变化
     mapData: {
       handler(newData, oldData) {
         if (!newData) return;
-        
-        // 简单判断:如果是首次加载数据,或者配置变了,需要重新渲染静态配置
         if (!oldData || JSON.stringify(newData.armsConfig) !== JSON.stringify(oldData.armsConfig)) {
           this.renderStaticConfig();
         }
-        // 每次数据变化都更新信号状态(倒计时和红绿灯)
         this.updateDynamicSignals();
       },
-      deep: true // 开启深度监听
+      deep: true
     }
   },
   methods: {
-    // ================= 自适应缩放 =================
-    fitToContainer() {
-      if (!this.stage || !this.$refs.konvaContainer) return;
-      const container = this.$refs.konvaContainer;
-      const containerWidth = container.clientWidth;
-      const containerHeight = container.clientHeight;
-      if (containerWidth === 0 || containerHeight === 0) return;
-
-      const designSize = this.sizeConfig.stageSize;
-      const scale = Math.min(containerWidth / designSize, containerHeight / designSize);
-
-      this.stage.width(containerWidth);
-      this.stage.height(containerHeight);
-
-      const offsetX = (containerWidth - designSize * scale) / 2;
-      const offsetY = (containerHeight - designSize * scale) / 2;
-
-      this.layer.scale({ x: scale, y: scale });
-      this.layer.position({ x: offsetX, y: offsetY });
-      this.layer.draw();
-    },
-
-    // ================= 初始化画布 =================
     initKonvaStage() {
       const { stageSize, halfRoad, roadWidth } = this.sizeConfig;
       const center = stageSize / 2;
 
-      const container = this.$refs.konvaContainer;
-      const initWidth = container.clientWidth || stageSize;
-      const initHeight = container.clientHeight || stageSize;
-
       this.stage = new Konva.Stage({
         container: this.$refs.konvaContainer,
-        width: initWidth,
-        height: initHeight
+        width: stageSize,
+        height: stageSize
       });
       this.layer = new Konva.Layer();
       this.stage.add(this.layer);
 
-      // 绘制背景
       this.layer.add(new Konva.Rect({ width: stageSize, height: stageSize, fill: this.C.BG }));
-      // 绘制中心路口交叉区
       this.layer.add(new Konva.Rect({ x: center - halfRoad, y: center - halfRoad, width: roadWidth, height: roadWidth, fill: this.C.ROAD }));
 
-      // 创建四个方向的道路骨架
       this.armsNodes = {
         N: this.createRoadArm(center, center - halfRoad, 0),
         E: this.createRoadArm(center + halfRoad, center, 90),
@@ -133,18 +88,50 @@ export default {
       };
       Object.values(this.armsNodes).forEach(arm => this.layer.add(arm));
 
-      // 创建中央面板
       this.createCenterPanel(center);
       this.layer.draw();
-      this.fitToContainer();
     },
 
-    // ================= 创建道路骨架 (内置绘制逻辑) =================
+    initResizeObserver() {
+      this.resizeObserver = new ResizeObserver(() => {
+        // 使用 requestAnimationFrame 防抖,让渲染更平滑
+        window.requestAnimationFrame(() => {
+          this.handleResize();
+        });
+      });
+      if (this.$refs.wrapper) {
+        this.resizeObserver.observe(this.$refs.wrapper);
+      }
+    },
+
+    handleResize() {
+      if (!this.stage || !this.$refs.wrapper) return;
+
+      // 现在拿到的是纯粹的父容器尺寸,绝对不会被 Canvas 撑大
+      const containerWidth = this.$refs.wrapper.clientWidth;
+      const containerHeight = this.$refs.wrapper.clientHeight;
+
+      // 如果容器被隐藏或没尺寸,则跳过
+      if (containerWidth === 0 || containerHeight === 0) return;
+
+      const scaleX = containerWidth / this.sizeConfig.stageSize;
+      const scaleY = containerHeight / this.sizeConfig.stageSize;
+      
+      // 取宽和高中最小的缩放比,保证图形能完整显示在容器内且不变形
+      const scale = Math.min(scaleX, scaleY);
+
+      // 缩放舞台物理尺寸
+      this.stage.width(this.sizeConfig.stageSize * scale);
+      this.stage.height(this.sizeConfig.stageSize * scale);
+      
+      // 缩放内部虚拟坐标系
+      this.stage.scale({ x: scale, y: scale });
+    },
+
     createRoadArm(x, y, rotation) {
       const { halfRoad, roadWidth, laneWidth } = this.sizeConfig;
       const group = new Konva.Group({ x, y, rotation });
       
-      // 1. 路面与线条
       group.add(new Konva.Rect({ x: -halfRoad, y: -350, width: roadWidth, height: 350, fill: this.C.ROAD }));
       group.add(new Konva.Line({ points: [0, -350, 0, -35], stroke: this.C.YELLOW, strokeWidth: 3 }));
       group.add(new Konva.Path({ data: `M -160 -350 L -160 -30 Q -160 0 -180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
@@ -158,7 +145,6 @@ export default {
         group.add(new Konva.Line({ points: [ox, -35, ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
       }
       
-      // 2. 信号灯带容器
       const lightGroup = new Konva.Group();
       const rectOpts = { y: -16, width: 8, height: 24, cornerRadius: 2, offsetX: 4, offsetY: 12 };
       for (let lx = -148; lx <= -20; lx += 16) lightGroup.add(new Konva.Rect({ x: lx, ...rectOpts }));
@@ -166,7 +152,6 @@ export default {
       group.add(lightGroup);
       group.lightGroup = lightGroup;
 
-      // 预留动态挂载点
       group.arrowNodes = { 0: null, 1: null, 2: null, 3: null };
       group.cameraNode = null;
 
@@ -190,7 +175,6 @@ export default {
       this.layer.add(panelGroup);
     },
 
-    // ================= 图标工厂函数 =================
     createArrowIcon(type, x, y, color = this.C.WHITE) {
       const group = new Konva.Group({ x, y, scaleX: 0.65, scaleY: 0.65 });
       group.add(new Konva.Circle({ x: 0, y: -35, radius: 3, fill: color, name: 'colorFill' }));
@@ -221,8 +205,6 @@ export default {
       return group;
     },
 
-    // ================= 核心更新逻辑 =================
-    // 渲染或更新静态设备(摄像头、箭头配置)
     renderStaticConfig() {
       const config = this.mapData.armsConfig;
       if (!config) return;
@@ -231,7 +213,6 @@ export default {
         const armData = config[dir];
         const armNode = this.armsNodes[dir];
 
-        // 挂载摄像头
         if (armNode.cameraNode) armNode.cameraNode.destroy();
         if (armData.cameraType > 0) {
           const cam = this.createCameraIcon(armData.cameraType, -80, -190);
@@ -239,7 +220,6 @@ export default {
           armNode.cameraNode = cam;
         }
 
-        // 挂载车道箭头
         armData.lanes.forEach((type, index) => {
           if (armNode.arrowNodes[index]) armNode.arrowNodes[index].destroy();
           if (type) {
@@ -253,7 +233,6 @@ export default {
       this.layer.draw();
     },
 
-    // 根据实时倒计时和红绿灯更新颜色
     updateDynamicSignals() {
       const signals = this.mapData.signals;
       if (!signals) return;
@@ -261,7 +240,6 @@ export default {
       const nsColor = signals.ns.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
       const ewColor = signals.ew.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
 
-      // 辅助函数:染色
       const dyeArm = (armNode, color) => {
         armNode.lightGroup.getChildren().forEach(r => r.fill(color));
         Object.values(armNode.arrowNodes).forEach(arr => {
@@ -277,7 +255,6 @@ export default {
       dyeArm(this.armsNodes.E, ewColor);
       dyeArm(this.armsNodes.W, ewColor);
 
-      // 更新中央面板
       this.panelNodes.nsLabel.text(`${signals.ns.phaseName}:`);
       this.panelNodes.nsVal.text(signals.ns.time.toString().padStart(2, '0')).fill(nsColor);
       
@@ -291,8 +268,18 @@ export default {
 </script>
 
 <style scoped>
-.intersection-map-container {
+/* 定义外层包裹容器 */
+.map-wrapper {
   width: 100%;
   height: 100%;
+  overflow: hidden;
+  background-color: #212842;
+  position: relative; /* 让内部绝对定位元素以此为参考 */
+}
+.konva-container {
+  position: absolute; /* 必须有,让 Canvas 脱离文档流 */
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
 }
 </style>

+ 73 - 10
src/components/IntersectionSignalMonitoring.vue

@@ -1,9 +1,13 @@
 <template>
-    <div class="container">
-        <div>
+    <div class="container" ref="Container">
+        <div class="intersection" ref="intersectionBox">
+            <video class="video-1" :src="video1" autoplay muted loop></video>
+            <video class="video-2" :src="video2" autoplay muted loop></video>
+            <video class="video-3" :src="video1" autoplay muted loop></video>
+            <video class="video-4" :src="video2" autoplay muted loop></video>
             <IntersectionMap :mapData="intersectionData" />
         </div>
-        <div>
+        <div class="signaltiming" ref="Signaltiming">
             <SignalTimingChart 
                 :loading="loading" 
                 :cycle-length="signalTimingData.cycleLength"
@@ -18,6 +22,8 @@
 import SignalTimingChart from '@/components/SignalTimingChart.vue';
 import IntersectionMap from '@/components/IntersectionMap.vue';
 import { fetchSignalTimingData, getIntersectionData } from '@/mock/data';
+import video1 from '@/assets/videos/video1.mp4';
+import video2 from '@/assets/videos/video2.mp4';
 
 export default {
     name: "IntersectionSignalMonitoring",
@@ -36,7 +42,11 @@ export default {
             signalTimingData: {},
             loading: false,
             intersectionData: {},
-            timer: null
+            timer: null,
+            mapWidth: 600,
+            mapHeight: 600,
+            video1,
+            video2
         };
     },
     computed: {
@@ -45,20 +55,46 @@ export default {
     created() {
     },
     async mounted() {
+        this.measureIntersectionBox();
+
+        this._roaPending = false;
+        this._resizeObserver = new ResizeObserver(() => {
+            if (!this._roaPending) {
+                this._roaPending = true;
+                requestAnimationFrame(() => {
+                    this._roaPending = false;
+                    this.measureIntersectionBox();
+                });
+            }
+        });
+        this._resizeObserver.observe(this.$refs.Container);
+
         this.loading = true;
-        // 1. 发起 API 请求获取初始化数据
         const signalRes = await fetchSignalTimingData(this.nodeData.id);
         this.signalTimingData = signalRes.data;
         this.intersectionData = await getIntersectionData(this.nodeData.id);
         this.loading = false;
 
-        // 2. 开启前端模拟倒计时(实际项目中这个可能由 WebSocket 每秒推送过来)
         this.startSimulationTimer();
     },
     beforeDestroy() {
+    if (this._resizeObserver) this._resizeObserver.disconnect();
     if (this.timer) clearInterval(this.timer);
   },
   methods: {
+    measureIntersectionBox() {
+      const container = this.$refs.Container;
+      const signaltiming = this.$refs.Signaltiming;
+      if (!container) return;
+      // 容器总高度 - padding(上下各15) - gap(12) - signaltiming高度(300) = intersection可用高度
+      const containerH = container.clientHeight;
+      const containerW = container.clientWidth;
+      const signalH = signaltiming ? signaltiming.clientHeight : 300;
+      const padding = 30; // 上下 padding 各 15px
+      const gap = 12;
+      this.mapWidth = containerW - padding;
+      this.mapHeight = containerH - padding - gap - signalH;
+    },
     startSimulationTimer() {
       this.timer = setInterval(() => {
         // 创建数据的副本以触发 Vue 的响应式更新
@@ -91,15 +127,42 @@ export default {
 
 <style scoped>
 .container {
-    display: flex;
-    flex-direction: column;
     width: 100%;
     height: 100%;
-    gap: 12px;
     background-color: #212842;
+    padding: 15px;
+    overflow: hidden;
 }
-.container > div {
+.container .intersection {
     flex: 1;
     width: 100%;
+    height: 350px;
+    position: relative;
+}
+.container .intersection video {
+    position: absolute;
+    z-index: 10;
+    height: 110px;
+    width: 200px;
+}
+.container .intersection .video-1 {
+    left: 78px;
+    top: 0;
+}
+.container .intersection .video-2 {
+    right: 78px;
+    top: 0;
+}
+.container .intersection .video-3 {
+    left: 78px;
+    bottom: 0;
+}
+.container .intersection .video-4 {
+    right: 78px;
+    bottom: 0;
+}
+.container .signaltiming {
+    margin-top: 12px;
+    width: 100%;
 }
 </style>

+ 0 - 3
src/components/SignalTimingChart.vue

@@ -272,9 +272,6 @@ export default {
   width: 100%;
   height: 100%;
   background-color: transparent;
-  padding: 15px 20px;
-  border-radius: 4px;
-  font-family: 'Microsoft YaHei', sans-serif;
   box-sizing: border-box;
   position: relative;
   display: flex;

+ 4 - 10
src/views/MainWatch.vue

@@ -148,7 +148,7 @@
                 :node-data="dialog.nodeData" />
             </div>
             <div class="big-mointoring-right">
-              aaaa
+              
             </div>
           </div>
         </template>
@@ -281,12 +281,13 @@ export default {
       } else if (this.currentTab === 'crossing') {
         currentComponentName = 'BigIntersectionSignalMonitoring';
         width = 1500;
+        height = 650;
         center = false;
         // 弹窗位置:紧贴 MenuItem 右侧,间距 50px
         if (nodeData.rect) {
           position = {
             x: nodeData.rect.right + 50,
-            y: nodeData.rect.y
+            y: nodeData.rect.y - 80
           };
         }
 
@@ -614,10 +615,6 @@ export default {
 .map-frame{
   position: relative;
   height:100%;
-  border-radius: 16px;
-  border: 1px solid rgba(80,200,255,0.18);
-  background: linear-gradient(180deg, rgba(8,22,60,0.42), rgba(8,22,60,0.18));
-  box-shadow: 0 0 28px rgba(0,160,255,0.08);
   position:relative;
   overflow:hidden;
 }
@@ -664,9 +661,6 @@ export default {
   padding-bottom: calc(var(--s) * 120px);
   position:absolute;
   inset:0;
-  background-image:
-    radial-gradient(900px 520px at 50% 35%, rgba(0,220,255,0.10), rgba(0,0,0,0)),
-    radial-gradient(720px 420px at 30% 75%, rgba(30,120,255,0.10), rgba(0,0,0,0));
   background-size: cover;
   background-position:center;
 }
@@ -1114,7 +1108,7 @@ export default {
 }
 .big-mointoring-left {
   flex: 1;
-  max-width: 200px;
+  width: 70%;
 }
 .big-mointoring-right {
   flex: 1;