Explorar el Código

重置SignalTimingchart组件;新增CrossingPanel路口小弹窗组件;调整状态监控页面的路口菜单点击后的显示弹窗内容;

画安 hace 3 días
padre
commit
4be00fef94

+ 207 - 0
src/components/ui/CrossingPanel.vue

@@ -0,0 +1,207 @@
+<template>
+    <div class="crossing-panel">
+        <div class="intersection-video-wrap">
+            <IntersectionMapVideos :mapData="intersectionData" :videoUrls="currentRoute.cornerVideos" />
+        </div>
+        <div class="signal-timing-wrap">
+            <div class="header" v-if="false">
+                <div class="title-area">
+                    <span class="main-title">方案状态</span>
+                    <span class="sub-info">(周期: {{ cycleLength }} 相位差: 0 协调时间: 0)</span>
+                </div>
+                <div class="checkbox-area" v-if="false">
+                    <div class="checkbox-mock" :class="{ 'is-checked': followPhase }"
+                        @click="followPhase = !followPhase">
+                        <span v-if="followPhase" style="color: #fff; font-size: 12px; margin-left: 1px;">✓</span>
+                    </div>
+                    <span>跟随相位</span>
+                </div>
+            </div>
+
+            <SignalTimingChart :cycleLength="cycleLength" :currentTime="currentSec" :phaseData="mockPhaseData" />
+        </div>
+    </div>
+</template>
+
+<script>
+import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
+import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
+
+import { getIntersectionData } from '@/mock/data';
+
+export default {
+    name: 'CrossingPanel',
+    components: {
+        SignalTimingChart,
+        IntersectionMapVideos
+    },
+    props: {
+
+    },
+    data() {
+        return {
+            followPhase: false,
+            intersectionData: {},
+            currentRoute: {
+                id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
+                mainVideo: require('@/assets/videos/video1.mp4'),
+                cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+            },
+            cycleLength: 140,
+            currentSec: 15,
+            mockPhaseData: [
+                // ================= 上轨道 (Track 0) =================
+                // S1阶段 (0-30s): P1 直行
+                [0, 0, 23, 'P1', 30, 'green', 'UP'],
+                [0, 23, 26, '', 3, 'stripe', ''],
+                [0, 26, 29, '', 3, 'yellow', ''],
+                [0, 29, 30, '', 1, 'red', ''],
+
+                // S2阶段 (30-60s): P2 左转
+                [0, 30, 53, 'P2', 30, 'green', 'TURN_LEFT'],
+                [0, 53, 56, '', 3, 'stripe', ''],
+                [0, 56, 59, '', 3, 'yellow', ''],
+                [0, 59, 60, '', 1, 'red', ''],
+
+                // S3阶段 (60-110s): P3 侧向左转 (使用向左箭头)
+                [0, 60, 103, 'P3', 50, 'green', 'TURN_LEFT'],
+                [0, 103, 106, '', 3, 'stripe', ''],
+                [0, 106, 109, '', 3, 'yellow', ''],
+                [0, 109, 110, '', 1, 'red', ''],
+
+                // S4阶段 (110-140s): P4 掉头
+                [0, 110, 133, 'P4', 30, 'green', 'UTURN'],
+                [0, 133, 136, '', 3, 'stripe', ''],
+                [0, 136, 139, '', 3, 'yellow', ''],
+                [0, 139, 140, '', 1, 'red', ''],
+
+                // ================= 下轨道 (Track 1) =================
+                // S1阶段 (0-30s): P5 直行
+                [1, 0, 23, 'P5', 30, 'green', 'UP'],
+                [1, 23, 26, '', 3, 'stripe', ''],
+                [1, 26, 29, '', 3, 'yellow', ''],
+                [1, 29, 30, '', 1, 'red', ''],
+
+                // S2阶段 (30-60s): P6 左转
+                [1, 30, 53, 'P6', 30, 'green', 'TURN_LEFT'],
+                [1, 53, 56, '', 3, 'stripe', ''],
+                [1, 56, 59, '', 3, 'yellow', ''],
+                [1, 59, 60, '', 1, 'red', ''],
+
+                // S3阶段 (60-110s): P7 侧向右转 (使用向右箭头)
+                [1, 60, 103, 'P7', 50, 'green', 'TURN_RIGHT'],
+                [1, 103, 106, '', 3, 'stripe', ''],
+                [1, 106, 109, '', 3, 'yellow', ''],
+                [1, 109, 110, '', 1, 'red', ''],
+
+                // S4阶段 (110-140s): P8 左转
+                [1, 110, 133, 'P8', 30, 'green', 'TURN_LEFT'],
+                [1, 133, 136, '', 3, 'stripe', ''],
+                [1, 136, 139, '', 3, 'yellow', ''],
+                [1, 139, 140, '', 1, 'red', '']
+            ]
+        }
+    },
+    async mounted() {
+        this.intersectionData = await getIntersectionData();
+    },
+}
+</script>
+
+<style scoped>
+.crossing-panel {
+    display: flex;
+    flex-direction: column;
+}
+
+.intersection-video-wrap {
+    width: 100%;
+    height: 100px;
+    flex: auto;
+}
+
+.signal-timing-wrap {
+    flex: 1;
+    --s: 1;
+    width: 100%;
+    height: 80px;
+    min-width: 0;
+    background-color: transparent;
+    box-sizing: border-box;
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    padding: calc(var(--s) * 10px);
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: calc(var(--s) * 15px);
+    color: #e0e6f1;
+    flex-shrink: 0;
+}
+
+.title-area {
+    font-size: calc(var(--s) * 16px);
+}
+
+.main-title {
+    font-size: calc(var(--s) * 18px);
+    font-weight: bold;
+    margin-right: calc(var(--s) * 10px);
+}
+
+.sub-info {
+    font-size: calc(var(--s) * 12px);
+    opacity: 0.8;
+}
+
+.checkbox-area {
+    font-size: calc(var(--s) * 12px);
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    opacity: 0.7;
+    user-select: none;
+}
+
+.checkbox-area:hover {
+    opacity: 1;
+}
+
+.checkbox-mock {
+    width: calc(var(--s) * 14px);
+    height: calc(var(--s) * 14px);
+    border: 1px solid rgba(255, 255, 255, 0.5);
+    margin-right: calc(var(--s) * 6px);
+    border-radius: 2px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.checkbox-mock.is-checked {
+    background-color: #4da8ff;
+    border-color: #4da8ff;
+}
+
+.chart-container {
+    width: 100%;
+    min-width: 0;
+    flex: 1;
+    min-height: 80px;
+    overflow: hidden;
+}
+
+.loading-overlay {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #758599;
+    font-size: 14px;
+}
+</style>

+ 243 - 0
src/components/ui/SignalTimingChart.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="signal-timing-chart">
+    <div ref="chartRef" class="chart-container"></div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+// 引入你的全局自适应 Mixin
+import echartsResize from '@/mixins/echartsResize.js';
+
+// 静态资源与颜色常量保持不变
+const COLORS = {
+  GREEN_LIGHT: '#8dc453', GREEN_DARK: '#73a542', YELLOW: '#fbd249', RED: '#ff7575', STRIPE_GREEN: '#a3d76e',
+  TEXT_DARK: '#1e2638', AXIS_LINE: '#758599', DIVIDER_LINE: '#111827', MARK_BLUE: '#4da8ff', TEXT_LIGHT: '#d1d5db'
+};
+
+const stripeCanvas = document.createElement('canvas');
+stripeCanvas.width = 6; stripeCanvas.height = 20;
+const ctx = stripeCanvas.getContext('2d');
+ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 6, 20);
+ctx.fillStyle = COLORS.STRIPE_GREEN; ctx.fillRect(0, 0, 3, 20); 
+const stripePattern = { image: stripeCanvas, repeat: 'repeat' };
+
+const ICON_PATHS = {
+  UP: 'M10 2 H14 V14 H20 L12 22 L4 14 H10 Z',
+  DOWN: 'M10 22 H14 V10 H20 L12 2 L4 10 H10 Z',
+  TURN_LEFT: 'M 21 22 H 15 V 14 C 15 11 13 9 10 9 H 8 V 14 L 0 7 L 8 0 V 5 H 10 C 15 5 21 9 21 14 V 22 Z',
+  TURN_RIGHT: 'M 3 22 H 9 V 14 C 9 11 11 9 14 9 H 16 V 14 L 24 7 L 16 0 V 5 H 14 C 9 5 3 9 3 14 V 22 Z',
+  UTURN: 'M 18 22 V 10 C 18 5 15 2 12 2 C 9 2 6 5 6 10 V 14 H 0 L 8 22 L 16 14 H 10 V 10 C 10 7 11 6 12 6 C 13 6 14 7 14 10 V 22 H 18 Z'
+};
+
+export default {
+  name: 'SignalTimingChart',
+  mixins: [echartsResize], // 注册自适应 mixin
+  props: {
+    cycleLength: { type: Number, default: 0 },
+    currentTime: { type: Number, default: 0 },
+    phaseData: { type: Array, default: () => [] }
+  },
+  data() {
+    return {
+      scaleFactor: 1
+    };
+  },
+  mounted() {
+    this.initChart();
+  },
+  watch: {
+    currentTime() {
+      // 使用 mixin 中的 $_chart 实例
+      if (this.$_chart) {
+        this.updateChart();
+      }
+    },
+    phaseData: {
+      deep: true,
+      handler(newVal) {
+        if (this.$_chart && newVal.length > 0) {
+          this.updateChart();
+        }
+      }
+    }
+  },
+  methods: {
+    // 动态计算缩放比例
+    updateScale() {
+      const el = this.$el;
+      if (!el) return;
+      const baseWidth = 600; 
+      // 限制最小缩放为 0.5,防止极端小弹窗下彻底糊掉
+      this.scaleFactor = Math.max(0.5, el.clientWidth / baseWidth);
+    },
+    
+    initChart() {
+      const chartDom = this.$refs.chartRef;
+      if (!chartDom) return;
+      
+      this.updateScale();
+      // 初始化 mixin 中的 $_chart
+      this.$_chart = echarts.init(chartDom);
+      
+      if (this.phaseData.length > 0) {
+        this.updateChart();
+      }
+    },
+    
+    // 该方法会自动被 echartsResize mixin 触发
+    updateChart() {
+      if (!this.$_chart) return;
+      
+      // 重绘前更新当前最新的尺寸比例
+      this.updateScale();
+      this.$_chart.setOption(this.getChartOption(), true);
+    },
+    
+    getChartOption() {
+      const s = this.scaleFactor;
+      return {
+        backgroundColor: 'transparent',
+        // 因为去掉了头部,稍微减小了 top 的留白,让图表更紧凑
+        grid: { 
+          left: Math.round(10 * s), 
+          right: Math.round(10 * s), 
+          top: Math.round(30 * s), 
+          bottom: Math.round(10 * s), 
+          containLabel: false 
+        },
+        xAxis: { type: 'value', min: 0, max: this.cycleLength, show: false },
+        yAxis: { type: 'category', data: ['Track 0', 'Track 1'], inverse: true, show: false },
+        series: [
+          {
+            type: 'custom',
+            // 箭头函数确保 this 指向 Vue 实例,以拿到 scaleFactor
+            renderItem: (params, api) => this.renderCustomItem(params, api),
+            encode: { x: [1, 2], y: 0 },
+            data: this.phaseData,
+            markLine: {
+              symbol: ['none', 'none'],
+              silent: true,
+              label: {
+                show: true,
+                position: 'start',
+                formatter: `${this.currentTime}/${this.cycleLength}`,
+                color: '#fff',
+                backgroundColor: COLORS.MARK_BLUE,
+                padding: [Math.round(4 * s), Math.round(8 * s)],
+                borderRadius: 2,
+                fontSize: Math.max(10, Math.round(10 * s)),
+                fontWeight: 'bold',
+                offset: [0, -2]
+              },
+              lineStyle: { color: COLORS.MARK_BLUE, type: 'solid', width: Math.max(1, Math.round(2 * s)) },
+              data: [ { xAxis: this.currentTime } ]
+            }
+          }
+        ]
+      };
+    },
+
+    // 核心绘图逻辑保持不变,确保全链路使用 this.scaleFactor
+    renderCustomItem(params, api) {
+      const s = this.scaleFactor;
+      const trackIndex = api.value(0);
+      const start = api.coord([api.value(1), trackIndex]);
+      const end = api.coord([api.value(2), trackIndex]);
+
+      const blockHeight = api.size([0, 1])[1];
+      const yPos = start[1] - blockHeight / 2;
+      const blockWidth = end[0] - start[0];
+      const dividerY = (api.coord([0, 1])[1] + api.coord([0, 0])[1]) / 2;
+
+      const phaseName = api.value(3);
+      const duration = api.value(4);
+      const type = api.value(5);
+      const iconKey = api.value(6);
+
+      let fillStyle = COLORS.GREEN_LIGHT;
+      if (type === 'stripe') fillStyle = stripePattern;
+      else if (type === 'yellow') fillStyle = COLORS.YELLOW;
+      else if (type === 'red') fillStyle = COLORS.RED;
+
+      const rectShape = echarts.graphic.clipRectByRect(
+        { x: start[0], y: yPos, width: blockWidth, height: blockHeight },
+        { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }
+      );
+
+      if (!rectShape) return;
+      const children = [];
+
+      // A. 绘制刻度
+      if (params.dataIndex === 0) {
+        const axisBaseY = params.coordSys.y - Math.round(20 * s);
+        [0, 35, 70, 105, 140].forEach(val => {
+          const x = api.coord([val, 0])[0];
+          children.push({ type: 'line', shape: { x1: x, y1: axisBaseY - Math.round(5 * s), x2: x, y2: axisBaseY + Math.round(5 * s) }, style: { stroke: COLORS.AXIS_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
+        });
+        const stages = [ {n:'S1', s:0, e:35}, {n:'S2', s:35, e:70}, {n:'S3', s:70, e:105}, {n:'S4', s:105, e:140} ];
+        stages.forEach(st => {
+          const x1 = api.coord([st.s, 0])[0], x2 = api.coord([st.e, 0])[0], midX = (x1 + x2) / 2;
+          const textHalf = Math.round(14 * s);
+          children.push({ type: 'line', shape: { x1: x1, y1: axisBaseY, x2: midX - textHalf, y2: axisBaseY }, style: { stroke: COLORS.AXIS_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
+          children.push({ type: 'line', shape: { x1: midX + textHalf, y1: axisBaseY, x2: x2, y2: axisBaseY }, style: { stroke: COLORS.AXIS_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
+          children.push({ type: 'text', style: { text: st.n, x: midX, y: axisBaseY, fill: COLORS.TEXT_LIGHT, fontSize: Math.max(10, Math.round(14 * s)), align: 'center', verticalAlign: 'middle', fontWeight: 'bold' } });
+        });
+      }
+
+      // B. 绘制色块底色
+      children.push({ type: 'rect', shape: rectShape, style: { fill: fillStyle, stroke: 'none' } });
+
+      // C. 绘制内部元素
+      const fs = Math.max(0.8, s * 0.9); // 文字/图标缩放
+      if (type === 'green' && blockWidth > 20) {
+        const darkWidth = Math.round(25 * fs);
+        const midY = yPos + blockHeight / 2;
+
+        children.push({ type: 'rect', shape: { x: start[0], y: yPos, width: darkWidth, height: blockHeight }, style: { fill: COLORS.GREEN_DARK } });
+        const arrowH = Math.round(4 * fs);
+        children.push({ type: 'polygon', shape: { points: [ [start[0] + darkWidth, midY - arrowH], [start[0] + darkWidth, midY + arrowH], [start[0] + darkWidth + arrowH, midY] ] }, style: { fill: COLORS.GREEN_DARK } });
+
+        if (iconKey && ICON_PATHS[iconKey]) {
+          const iconSize = Math.round(14 * fs);
+          const iconX = start[0] + (darkWidth - iconSize) / 2;
+          const iconY = midY - iconSize / 2;
+
+          children.push({
+            type: 'path',
+            shape: { pathData: ICON_PATHS[iconKey], x: iconX, y: iconY, width: iconSize, height: iconSize, layout: 'center' },
+            style: { fill: COLORS.TEXT_DARK, stroke: 'none' }
+          });
+        }
+
+        children.push({ type: 'text', style: { text: `${phaseName}\n${duration}`, x: start[0] + darkWidth + Math.round(4 * fs), y: midY, fill: COLORS.TEXT_DARK, fontSize: Math.max(10, Math.round(12 * fs)), fontFamily: 'Arial', fontWeight: 'bold', align: 'left', verticalAlign: 'middle' } });
+      }
+
+      // D. 分割线
+      if (trackIndex === 1) {
+        children.push({ type: 'line', shape: { x1: start[0], y1: dividerY, x2: end[0], y2: dividerY }, style: { stroke: COLORS.DIVIDER_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
+      }
+
+      return { type: 'group', children: children };
+    }
+  }
+};
+</script>
+
+<style scoped>
+.signal-timing-chart {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.chart-container { 
+  width: 100%; 
+  flex: 1; 
+  min-height: 80px; 
+  overflow: hidden; 
+}
+</style>

+ 42 - 33
src/mixins/echartsResize.js

@@ -1,59 +1,68 @@
-// 【核心修改】:弃用引入的死变量,直接使用动态计算
-const DESIGN_WIDTH = 1920; // 替换为你大屏的实际设计稿宽度
+const DESIGN_WIDTH = 1920; 
 
-// 实时计算的转换工具
 export function px2echarts(px) {
   const scale = window.innerWidth / DESIGN_WIDTH;
-  return Math.round(px * scale); // 加 round 保证像素点为整数,防止字体虚化
+  return Math.round(px * scale); 
 }
 
 export default {
   data() {
     return {
       $_chart: null,
-      $_resizeHandler: null
+      $_resizeObserver: null // 替换原来的 resizeHandler
     };
   },
   mounted() {
-    this.$_initResizeEvent();
+    // 确保 DOM 完全渲染后再进行监听
+    this.$nextTick(() => {
+      this.$_initResizeObserver();
+    });
   },
   beforeDestroy() {
-    this.$_destroyResizeEvent();
+    this.$_destroyResizeObserver();
     if (this.$_chart) {
       this.$_chart.dispose();
       this.$_chart = null;
     }
   },
   methods: {
-    $_initResizeEvent() {
-      // 防抖
-      const debounce = (fn, delay) => {
-        let timer = null;
-        return function () {
-          if (timer) clearTimeout(timer);
-          timer = setTimeout(() => { fn.apply(this, arguments); }, delay);
-        };
-      };
+    $_initResizeObserver() {
+      // 【智能挂载点】:优先寻找 ref="chartRef" 的专属图表容器,如果没有,则退而求其次监听整个组件根节点 ($el)
+      const targetDom = this.$refs.chartRef || this.$el;
+      
+      // 确保取到的是有效的 DOM 元素
+      if (!targetDom || targetDom.nodeType !== 1) return; 
 
-      this.$_resizeHandler = debounce(() => {
-        if (this.$_chart) {
-          // 1. 强制 ECharts 重绘画布物理大小
-          this.$_chart.resize();
-          
-          // 2. 【核心黑科技】:如果组件内部写了 updateChart 方法,自动触发它!
-          // 这样就可以让组件使用新的 px2echarts 重新 setOption
-          if (typeof this.updateChart === 'function') {
-            this.updateChart();
-          }
-        }
-      }, 100);
+      let timer = null;
 
-      window.addEventListener('resize', this.$_resizeHandler);
+      this.$_resizeObserver = new ResizeObserver(() => {
+        // 使用防抖 (Debounce) 避免拖拽弹窗时触发过于频繁导致页面卡顿
+        if (timer) clearTimeout(timer);
+        timer = setTimeout(() => {
+          // 结合 requestAnimationFrame,在浏览器下一次重绘前执行,动画更丝滑
+          // 并且能有效避免 "ResizeObserver loop limit exceeded" 这种控制台红字报错
+          requestAnimationFrame(() => {
+            if (this.$_chart) {
+              // 1. 强制 ECharts 重绘画布物理大小
+              this.$_chart.resize();
+              
+              // 2. 如果组件内部写了 updateChart 方法,自动触发它!
+              if (typeof this.updateChart === 'function') {
+                this.updateChart();
+              }
+            }
+          });
+        }, 50); // 50ms 是兼顾拖拽流畅度和 CPU 性能的黄金时间
+      });
+
+      // 开始监听
+      this.$_resizeObserver.observe(targetDom);
     },
-    $_destroyResizeEvent() {
-      if (this.$_resizeHandler) {
-        window.removeEventListener('resize', this.$_resizeHandler);
-        this.$_resizeHandler = null;
+
+    $_destroyResizeObserver() {
+      if (this.$_resizeObserver) {
+        this.$_resizeObserver.disconnect(); // 停止监听,释放内存
+        this.$_resizeObserver = null;
       }
     }
   }

+ 1 - 1
src/styles/base.css

@@ -100,7 +100,7 @@ html, body {
 .left-sidebar-wrap {
     max-width: 400px;
     position: relative;
-    top: -50px;
+    /* top: -50px; */
 }
 
 /* ==========================================

+ 25 - 16
src/views/StatusMonitoring.vue

@@ -76,6 +76,7 @@ import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
 import TrafficTimeSpace from '@/components/ui/TrafficTimeSpace.vue';
 import MenuItem from '@/components/ui/MenuItem.vue';
 import RingDonutChart from '@/components/ui/RingDonutChart.vue';
+import CrossingPanel from '@/components/ui/CrossingPanel.vue';
 import { getIntersectionData, makeTrafficTimeSpaceData } from '@/mock/data';
 
 
@@ -94,7 +95,8 @@ export default {
         SecurityRoutePanel,
         IntersectionMapVideos,
         TrafficTimeSpace,
-        RingDonutChart
+        RingDonutChart,
+        CrossingPanel
     },
     data() {
         return {
@@ -232,22 +234,15 @@ export default {
     mounted() {
         // this.openDialog({
         //         id: 'test', // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
-        //         title: '',
-        //         component: 'RingDonutChart',
-        //         width: 228,
-        //         height: 124,
+        //         title: 'dddd',
+        //         component: 'CrossingPanel',
+        //         width: 260,
+        //         height: 260,
         //         center: false,
-        //         showClose: false,
+        //         showClose: true,
         //         position: { x: 750, y: 130 },
         //         noPadding: true,
-        //         data: {
-        //             chartData: [
-        //                 { name: '在线', value: 38, color: '#4DF5F8' },
-        //                 { name: '离线', value: 3, color: '#FFD369' }
-        //             ],
-        //             centerTitle: "98%",
-        //             centerSubTitle: "38/41"
-        //         }
+        //         data: {}
         //     });
     },
     methods: {
@@ -324,7 +319,7 @@ export default {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
 
             this.openDialog({
-                id: 'crossing_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                id: 'crossing_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
                 title: '',
                 component: 'RingDonutChart',
                 width: 228,
@@ -344,7 +339,7 @@ export default {
             });
 
             this.openDialog({
-                id: 'crossing2_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                id: 'crossing2_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
                 title: '',
                 component: 'RingDonutChart',
                 width: 228,
@@ -365,6 +360,20 @@ export default {
                 }
             });
 
+            // 路口弹窗
+            this.openDialog({
+                id: 'crossing3_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
+                title: nodeData.label,
+                component: 'CrossingPanel',
+                width: 260,
+                height: 260,
+                center: false,
+                showClose: true,
+                position: { x: 950, y: 430 },
+                noPadding: false,
+                data: {}
+            });
+
 
         },
         // 显示干线弹窗组