Просмотр исходного кода

路口详情弹窗:加视频/检测器切换 + 检测器监视弹窗

    - IntersectionMapVideos 右上角 SegmentedRadio(视频/检测器),自适应
      缩放 + 多分屏隐藏阈值;切到检测器:4 方向显示 球机风格图标 + 编号
      ① + 流量/占有率两行(每 5s ±15%/±5 局部波动)
    - 切到检测器同时居中弹 DetectorTable「检测器运行数据监视」,弹窗 id
      用 _uid 隔离多分屏,用户关弹窗联动按钮切回视频
    - mock 加 _makeDetectors/_detectorsToArmConfig + 给预存 config 兜底;
      _makeCameras 改 4 方向统一枪机
    - 新增DetectorTable
    - 修 Vue 2 _ 前缀 data 不代理导致 fluctuateDetectors 报错
画安 месяцев назад: 2
Родитель
Сommit
33cad0e9ed

+ 197 - 0
src/components/ui/DetectorTable.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="detector-monitor-table">
+    <div class="table-header">
+      <div class="header-left">
+        <span v-for="tick in timeTicks" :key="tick" class="tick-item">{{ tick }}</span>
+      </div>
+      <div class="header-right">
+        <div class="th-cell">编号</div>
+        <div class="th-cell">名称</div>
+        <div class="th-cell">流量</div>
+        <div class="th-cell">占有率</div>
+      </div>
+    </div>
+
+    <div class="table-body">
+      <div class="table-row" v-for="item in tableData" :key="item.id">
+        <div class="row-left">
+          <!-- 留白:未来放每个检测器的时序曲线 -->
+        </div>
+
+        <div class="row-right">
+          <div class="td-cell id-cell">{{ item.id }}</div>
+          <div class="td-cell name-cell">{{ item.name }}</div>
+          <div class="td-cell num-cell flow">{{ item.flow }}</div>
+          <div class="td-cell num-cell occ">{{ item.occupancy }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "DetectorMonitorTable",
+  data() {
+    return {
+      timeTicks: [180, 150, 120, 90, 60, 30],
+      tableData: [
+        { id: 1, name: "东左转", flow: "50", occupancy: "25%" },
+        { id: 2, name: "东直行", flow: "60", occupancy: "35%" },
+        { id: 3, name: "东直行", flow: "70", occupancy: "25%" },
+        { id: 4, name: "西直行", flow: "58", occupancy: "35%" },
+      ]
+    };
+  }
+};
+</script>
+
+<style scoped>
+.detector-monitor-table {
+  width: 100%;
+  height: 100%;
+  padding: 12px 14px;
+  background: rgba(5, 22, 45, 0.9);
+  color: #fff;
+  font-family: inherit;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* ---------------- 表头 ---------------- */
+.table-header {
+  display: flex;
+  height: 34px;
+  background: linear-gradient(180deg, rgba(58, 127, 209, 0.28) 0%, rgba(58, 127, 209, 0.12) 100%);
+  border: 1px solid rgba(127, 182, 255, 0.4);
+  border-radius: 3px;
+  flex-shrink: 0;
+}
+
+.header-left {
+  flex: 1;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 16px;
+  border-right: 1px solid rgba(127, 182, 255, 0.35);
+}
+
+.tick-item {
+  font-size: 13px;
+  font-weight: 600;
+  color: rgba(127, 182, 255, 0.9);
+  font-family: 'Consolas', 'Courier New', monospace;
+  letter-spacing: 0.5px;
+}
+
+.header-right {
+  display: flex;
+  width: 260px;
+  flex-shrink: 0;
+}
+
+.th-cell {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  font-weight: 600;
+  color: rgba(127, 182, 255, 0.95);
+  border-right: 1px solid rgba(127, 182, 255, 0.25);
+}
+
+.th-cell:last-child {
+  border-right: none;
+}
+
+/* ---------------- 表体 ---------------- */
+.table-body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  margin-top: 4px;
+  overflow-y: auto;
+}
+
+.table-body::-webkit-scrollbar {
+  width: 6px;
+}
+
+.table-body::-webkit-scrollbar-thumb {
+  background: rgba(30, 77, 142, 0.6);
+  border-radius: 3px;
+}
+
+.table-body::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.table-row {
+  display: flex;
+  height: 44px;
+  flex-shrink: 0;
+  border-bottom: 1px dashed rgba(30, 77, 142, 0.45);
+  transition: background 0.15s;
+}
+
+.table-row:last-child {
+  border-bottom: none;
+}
+
+.table-row:hover {
+  background: rgba(58, 127, 209, 0.12);
+}
+
+.row-left {
+  flex: 1;
+  border-right: 1px solid rgba(30, 77, 142, 0.3);
+  /* 时序图区域占位(180s ~ 30s 时间窗),后续可挂折线/柱图 */
+}
+
+.row-right {
+  display: flex;
+  width: 260px;
+  flex-shrink: 0;
+}
+
+.td-cell {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  border-right: 1px solid rgba(30, 77, 142, 0.3);
+}
+
+.td-cell:last-child {
+  border-right: none;
+}
+
+.id-cell {
+  color: rgba(127, 182, 255, 0.95);
+  font-family: 'Consolas', 'Courier New', monospace;
+  font-weight: 600;
+}
+
+.name-cell {
+  color: rgba(255, 255, 255, 0.95);
+  font-weight: 500;
+}
+
+.num-cell {
+  font-family: 'Consolas', 'Courier New', monospace;
+  font-weight: 600;
+}
+
+.num-cell.flow {
+  color: #7fe3a8;
+}
+
+.num-cell.occ {
+  color: #ffc857;
+}
+</style>

+ 227 - 3
src/components/ui/IntersectionMapVideos.vue

@@ -2,6 +2,10 @@
   <div class="map-wrapper" ref="wrapper">
     <div class="konva-container" ref="konvaContainer"></div>
 
+    <div v-show="toggleVisible" class="display-mode-toggle" :style="toggleStyle">
+      <SegmentedRadio v-model="displayMode" :options="displayModeOptions" size="auto" />
+    </div>
+
     <div class="corner-videos-overlay" v-if="hasAnyVideo" :style="{ width: stageWidth + 'px', height: stageHeight + 'px' }">
       
       <div v-if="videoUrls.nw" class="video-corner top-left">
@@ -55,6 +59,16 @@
 <script>
 import Konva from 'konva';
 import XgVideoPlayer from '@/components/ui/XgVideoPlayer.vue';
+import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
+
+// 检测器锚点 = 原摄像机所在的 world 坐标(armNode 局部 (-80,-190) 经 arm 旋转后)。
+// 图标位于锚点上方,下方一行 [流量/占有率(右对齐) ①]。
+const DETECTOR_POSITIONS = {
+  N: { x: 370, y: 100 },
+  E: { x: 800, y: 370 },
+  S: { x: 530, y: 800 },
+  W: { x: 100, y: 530 },
+};
 
 // 方向箭头 SVG 路径映射
 // 所有方向臂共用同一套图标(以N方向/向下驶入为基准),由 createRoadArm 的 rotation 自动旋转
@@ -109,7 +123,12 @@ function loadSvgImage(svgUrl, fillColor) {
 export default {
   name: 'IntersectionMapVideos',
   components: {
-    XgVideoPlayer
+    XgVideoPlayer,
+    SegmentedRadio,
+  },
+  inject: {
+    // 用 default 兜底:IntersectionMapVideos 也可能被独立测试或其它非 DashboardLayout 父级使用
+    dialogManager: { default: null },
   },
   props: {
     // 1. 路口数字孪生数据
@@ -147,9 +166,28 @@ export default {
       stageWidth: 900,  // 当前画布缩放后的真实宽度
       stageHeight: 900, // 当前画布缩放后的真实高度
       activeVideos: { nw: false, ne: false, sw: false, se: false },
+      // 视频/检测器 切换
+      displayMode: 'video',
+      displayModeOptions: [
+        { label: '视频', value: 'video' },
+        { label: '检测器', value: 'detector' },
+      ],
+      toggleVisible: true,  // wrapper < 240px 时彻底隐藏(窗口太小放不下)
+      toggleScale: 1,        // 自适应缩放:wrapper 宽度低于全尺寸阈值时按比例缩小
+      // 检测器节点(直接挂在 layer 上,不进 armNode;位置见 DETECTOR_POSITIONS)
+      // 注意:detectorBase / detectorTimer 必须不带 `_` 前缀,否则 Vue 2 不会代理到 this(导致 undefined 错误)
+      detectorNodes: { N: null, E: null, S: null, W: null },
+      detectorBase: { N: null, E: null, S: null, W: null },
+      detectorTimer: null,
     };
   },
   computed: {
+    toggleStyle() {
+      return {
+        transform: `scale(${this.toggleScale})`,
+        transformOrigin: 'top right',
+      };
+    },
     // 判断是否传入了至少一个视频,如果没有,直接不渲染遮罩层提升性能
     hasAnyVideo() {
       if (!this.videoUrls) return false;
@@ -168,6 +206,8 @@ export default {
     this.initResizeObserver();
   },
   beforeDestroy() {
+    this.stopDetectorFluctuation();
+    this.closeDetectorDialog();
     if (this.resizeObserver) this.resizeObserver.disconnect();
     if (this.stage) this.stage.destroy();
   },
@@ -181,7 +221,10 @@ export default {
         this.updateDynamicSignals();
       },
       deep: true
-    }
+    },
+    displayMode() {
+      this.applyDisplayMode();
+    },
   },
   methods: {
     openVideo(corner) {
@@ -246,6 +289,14 @@ export default {
       this.stage.width(this.stageWidth);
       this.stage.height(this.stageHeight);
       this.stage.scale({ x: scale, y: scale });
+
+      // 自适应:wrapper >= 600 时全尺寸;240~600 之间线性缩放(最小 0.55);< 240 直接隐藏
+      if (containerWidth < 240) {
+        this.toggleVisible = false;
+      } else {
+        this.toggleVisible = true;
+        this.toggleScale = Math.min(1, Math.max(0.55, containerWidth / 600));
+      }
     },
 
     createRoadArm(x, y, rotation) {
@@ -325,6 +376,162 @@ export default {
       return group;
     },
 
+    /**
+     * 检测器条目布局(N 数据行在图标上方,其他方向在下方;编号在左、文字两行在右):
+     *   N 方向:       其他方向:
+     *     ①  流量:1230   [icon]
+     *        占有率:50%   ①  流量:1230
+     *     [icon]              占有率:50%
+     */
+    createDetectorIcon(num, dir, x, y) {
+      const group = new Konva.Group({ x, y });
+      const stroke = '#7fb6ff';
+      const fill = '#3a7fd1';
+
+      // 1) 顶部图标:沿用原 球机 造型(圆角矩形 + 内嵌镜头圆 + 底部支架横杆+竖杆)
+      const iconG = new Konva.Group({ x: 0, y: 2 });
+      iconG.add(new Konva.Rect({
+        x: -18, y: -32, width: 36, height: 32,
+        stroke, strokeWidth: 2.5, cornerRadius: 8,
+        fill: 'rgba(58,127,209,0.2)',
+      }));
+      iconG.add(new Konva.Circle({ x: 0, y: -16, radius: 7, stroke, strokeWidth: 2.5 }));
+      iconG.add(new Konva.Line({ points: [-13, 0, 13, 0], stroke, strokeWidth: 2.5, lineCap: 'round' }));
+      iconG.add(new Konva.Line({ points: [0, 0, 0, 8], stroke, strokeWidth: 2.5 }));
+      group.add(iconG);
+
+      // 2) 数据行 Y 偏移:N 在图标上方,其他在下方
+      const dataY = dir === 'N' ? -62 : 42;
+
+      // 3) 编号圆圈(左侧)
+      const badgeCx = -55;
+      group.add(new Konva.Circle({ x: badgeCx, y: dataY, radius: 13, fill, stroke, strokeWidth: 1.5 }));
+      group.add(new Konva.Text({
+        x: badgeCx - 13, y: dataY - 9, width: 26, align: 'center',
+        text: String(num), fontSize: 16, fontStyle: 'bold', fill: '#fff',
+      }));
+
+      // 4) 流量 / 占有率两行(编号右侧 6px 间隔)
+      const textX = badgeCx + 13 + 6;
+      const flowText = new Konva.Text({
+        x: textX, y: dataY - 18, text: '', fontSize: 18, fontStyle: 'bold', fill: '#fff', fontFamily: 'monospace',
+      });
+      const occText = new Konva.Text({
+        x: textX, y: dataY + 2, text: '', fontSize: 18, fontStyle: 'bold', fill: '#fff', fontFamily: 'monospace',
+      });
+      group.add(flowText, occText);
+      group.flowText = flowText;
+      group.occText = occText;
+      return group;
+    },
+
+    /** (重)创建四个方向的检测器节点;如已存在先 destroy */
+    renderDetectors() {
+      const cfg = (this.mapData && this.mapData.armsConfig) || {};
+      ['N', 'E', 'S', 'W'].forEach(dir => {
+        if (this.detectorNodes[dir]) {
+          this.detectorNodes[dir].destroy();
+          this.detectorNodes[dir] = null;
+        }
+        const det = cfg[dir] && cfg[dir].detector;
+        if (!det) return;
+        const pos = DETECTOR_POSITIONS[dir];
+        const node = this.createDetectorIcon(det.index, dir, pos.x, pos.y);
+        node.visible(this.displayMode === 'detector');
+        this.layer.add(node);
+        this.detectorNodes[dir] = node;
+      });
+      this.syncDetectorBase();
+      this.updateDetectorTexts();
+    },
+
+    /** 把 mapData 的检测器初值缓存为波动中心;显示值初始化为基础值 */
+    syncDetectorBase() {
+      const cfg = (this.mapData && this.mapData.armsConfig) || {};
+      ['N', 'E', 'S', 'W'].forEach(dir => {
+        const det = cfg[dir] && cfg[dir].detector;
+        this.detectorBase[dir] = det ? { flow: det.flow, occupancy: det.occupancy } : null;
+      });
+    },
+
+    /** 检测器模式:每 5s 让 flow ±15%、occupancy ±5 绝对值围绕基础值随机走步 */
+    fluctuateDetectors() {
+      ['N', 'E', 'S', 'W'].forEach(dir => {
+        const base = this.detectorBase[dir];
+        const node = this.detectorNodes[dir];
+        if (!base || !node) return;
+        const flow = Math.max(0, Math.round(base.flow + (Math.random() - 0.5) * 2 * base.flow * 0.15));
+        const occupancy = Math.max(0, Math.min(100, Math.round(base.occupancy + (Math.random() - 0.5) * 10)));
+        node.flowText.text(`流量:${flow}`);
+        node.occText.text(`占有率:${occupancy}%`);
+      });
+      if (this.layer) this.layer.batchDraw();
+    },
+
+    /** 用基础值刷一次显示文本(进入检测器模式或 mapData 切换时调用) */
+    updateDetectorTexts() {
+      ['N', 'E', 'S', 'W'].forEach(dir => {
+        const base = this.detectorBase[dir];
+        const node = this.detectorNodes[dir];
+        if (!base || !node) return;
+        node.flowText.text(`流量:${base.flow}`);
+        node.occText.text(`占有率:${base.occupancy}%`);
+      });
+      if (this.layer) this.layer.batchDraw();
+    },
+
+    startDetectorFluctuation() {
+      if (this.detectorTimer) return;
+      this.detectorTimer = setInterval(this.fluctuateDetectors, 5000);
+    },
+
+    stopDetectorFluctuation() {
+      if (this.detectorTimer) {
+        clearInterval(this.detectorTimer);
+        this.detectorTimer = null;
+      }
+    },
+
+    /** 切换 video/detector:用 visible() 而不是 destroy 重建,开销最小 */
+    applyDisplayMode() {
+      const showDetector = this.displayMode === 'detector';
+      ['N', 'E', 'S', 'W'].forEach(dir => {
+        const arm = this.armsNodes[dir];
+        if (arm && arm.cameraNode) arm.cameraNode.visible(!showDetector);
+        if (this.detectorNodes[dir]) this.detectorNodes[dir].visible(showDetector);
+      });
+      if (this.layer) this.layer.batchDraw();
+      if (showDetector) {
+        this.startDetectorFluctuation();
+        this.openDetectorDialog();
+      } else {
+        this.stopDetectorFluctuation();
+        this.closeDetectorDialog();
+      }
+    },
+
+    /** 检测器模式下打开弹窗:DetectorTable 居中展示「检测器运行数据监视」 */
+    openDetectorDialog() {
+      if (!this.dialogManager || typeof this.dialogManager.openDialog !== 'function') return;
+      this.dialogManager.openDialog({
+        id: `detector-monitor-${this._uid}`,
+        title: '检测器运行数据监视',
+        component: 'DetectorTable',
+        width: 620,
+        height: 360,
+        noPadding: false,
+        // 用户手动关闭弹窗时把按钮切回视频模式,保持二者状态同步
+        onClose: () => {
+          if (this.displayMode === 'detector') this.displayMode = 'video';
+        },
+      });
+    },
+
+    closeDetectorDialog() {
+      if (!this.dialogManager || typeof this.dialogManager.closeDialog !== 'function') return;
+      this.dialogManager.closeDialog(`detector-monitor-${this._uid}`);
+    },
+
     createCameraIcon(type, x, y) {
       const group = new Konva.Group({ x, y });
       if (type === 1) { 
@@ -368,6 +575,8 @@ export default {
           }
         });
       });
+      this.renderDetectors();
+      this.applyDisplayMode();
       this.layer.draw();
     },
 
@@ -456,13 +665,28 @@ export default {
 }
 
 .konva-container {
-  position: absolute; 
+  position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   z-index: 1; /* 图层垫底 */
 }
 
+.display-mode-toggle {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 50;                          /* 高于 konva-container(z=1),避免被画布盖住 */
+  width: 120px;
+  font-size: 12px;
+  background: rgba(5, 22, 45, 0.92);    /* 不透明深底,多窗口下按钮压住画布也能看清 */
+  border-radius: 4px;
+}
+.display-mode-toggle ::v-deep .radio-item {
+  padding: 2px 6px;
+  line-height: 1.3;
+}
+
 /* ================= 视频遮罩与挂件 ================= */
 .corner-videos-overlay {
   position: absolute;

+ 3 - 1
src/layouts/DashboardLayout.vue

@@ -110,6 +110,7 @@ import ChangePassword from '@/components/ui/ChangePassword.vue';
 import CrossingMultiView from '@/components/ui/CrossingMultiView.vue';
 import CrossingDetailHeader from '@/components/ui/CrossingDetailHeader.vue';
 import OfflineTip from '@/components/ui/OfflineTip.vue';
+import DetectorTable from '@/components/ui/DetectorTable.vue';
 import brand from '@/utils/brand';
 
 export default {
@@ -140,7 +141,8 @@ export default {
         ChangePassword,
         CrossingMultiView,
         CrossingDetailHeader,
-        OfflineTip
+        OfflineTip,
+        DetectorTable
     },
     provide() {
         return {

+ 60 - 10
src/mock/api.js

@@ -111,19 +111,18 @@ const DB = {
  */
 function _makeCameras(id, name, seed) {
   const positions = ['北进口', '南进口', '东进口', '西进口']
-  const types = ['枪机', '球机']
-  const numCams = 2 + (seed % 3) // 2~4 个
-  return Array.from({ length: numCams }, (_, i) => ({
+  // 4 方向各 1 个、统一枪机:视频模式下示意图 4 臂图标一致,避免 球机方框 与检测器视觉混淆
+  return positions.map((pos, i) => ({
     intersection: name || id,
     intersectionId: id,
     cameraId: `CAM${(id || '000').slice(-6)}_${String(i + 1).padStart(2, '0')}`,
     loginName: `admin_${String(seed + i).slice(-4)}`,
     password: '******',
-    cameraType: types[(seed + i) % types.length],
+    cameraType: '枪机',
     port: 554 + i,
     ip: `192.168.${10 + (seed % 200)}.${100 + i}`,
-    enabled: (seed + i) % 20 !== 0,  // ~5% 禁用
-    position: positions[i % positions.length],
+    enabled: true,
+    position: pos,
   }))
 }
 
@@ -141,6 +140,41 @@ function _camerasToArmTypes(cameras) {
   return result
 }
 
+/**
+ * 生成检测器模拟数据:四方向各 1 个,编号 1-4 顺时针(N=1, E=2, S=3, W=4)。
+ * flow / occupancy 给基础值;客户端在检测器模式下会基于此值做 ±15% / ±5 的随机走步。
+ */
+function _makeDetectors(id, name, seed) {
+  const dirs = [
+    { dir: 'N', label: '北进口', index: 1 },
+    { dir: 'E', label: '东进口', index: 2 },
+    { dir: 'S', label: '南进口', index: 3 },
+    { dir: 'W', label: '西进口', index: 4 },
+  ]
+  return dirs.map(p => ({
+    intersection: name || id,
+    intersectionId: id,
+    detectorId: `DT${(id || '000').slice(-6)}_${String(p.index).padStart(2, '0')}`,
+    index: p.index,
+    position: p.label,
+    direction: p.dir,
+    flow: 600 + ((seed * (p.index + 7)) % 1500),       // 600~2100
+    occupancy: 15 + ((seed * (p.index + 3)) % 60),     // 15~75 (%)
+    enabled: true,
+  }))
+}
+
+/** 从检测器列表推导各方向 { index, flow, occupancy } */
+function _detectorsToArmConfig(detectors) {
+  const out = { N: null, S: null, E: null, W: null }
+  detectors.forEach(d => {
+    if (d.direction && !out[d.direction]) {
+      out[d.direction] = { index: d.index, detectorId: d.detectorId, flow: d.flow, occupancy: d.occupancy }
+    }
+  })
+  return out
+}
+
 function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default' } = {}) {
   const phases = ['南北直行', '东西直行', '北单放', '东单放']
   const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false
@@ -150,6 +184,9 @@ function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default'
   const cameras = _makeCameras(id, name, seed)
   const armCamTypes = _camerasToArmTypes(cameras)
 
+  const detectors = _makeDetectors(id, name, seed)
+  const armDetectors = _detectorsToArmConfig(detectors)
+
   const lanesPresetMap = {
     default: {
       N: ['U', 'L', 'S', 'R'],
@@ -173,12 +210,13 @@ function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default'
       ew: { phaseName: phases[1], time: countdown, isGreen: !nsGreen },
     },
     armsConfig: {
-      N: { cameraType: armCamTypes.N, lanes: lanesPreset.N },
-      S: { cameraType: armCamTypes.S, lanes: lanesPreset.S },
-      E: { cameraType: armCamTypes.E, lanes: lanesPreset.E },
-      W: { cameraType: armCamTypes.W, lanes: lanesPreset.W },
+      N: { cameraType: armCamTypes.N, lanes: lanesPreset.N, detector: armDetectors.N },
+      S: { cameraType: armCamTypes.S, lanes: lanesPreset.S, detector: armDetectors.S },
+      E: { cameraType: armCamTypes.E, lanes: lanesPreset.E, detector: armDetectors.E },
+      W: { cameraType: armCamTypes.W, lanes: lanesPreset.W, detector: armDetectors.W },
     },
     cameras,
+    detectors,
   }
 }
 
@@ -929,6 +967,18 @@ export async function apiGetCrossingDetailData(id, { iconMode = 'default' } = {}
     config.status = _getDeviceStatus(id)
   }
 
+  // 确保 detectors 存在(mock_data.json 的预存配置没有此字段,需补齐)
+  if (!config.detectors) {
+    const detectors = _makeDetectors(id, point ? point.name : id, seed)
+    const armDetectors = _detectorsToArmConfig(detectors)
+    config.detectors = detectors
+    config.armsConfig = config.armsConfig || {}
+    ;['N', 'E', 'S', 'W'].forEach(d => {
+      config.armsConfig[d] = config.armsConfig[d] || {}
+      config.armsConfig[d].detector = armDetectors[d]
+    })
+  }
+
   // 从真实阶段数据推导周期和相位
   const cycleLength = _getCycleLength(id)
   const phaseData = _makePhaseData(cycleLength, false, 'simple', id)

+ 1 - 1
src/views/SpecialSituationMonitoring.vue

@@ -20,7 +20,7 @@
                 @map-crossing-mouseover="handleMapCrossingMouseover"
                 @map-crossing-mouseout="handleMapCrossingMouseout"
                 @bindTrunkMenuTree="handleTrunkMenuUpdate"
-                :show-search="true"
+                :show-search="false"
                 :search-offset-right="100"
                 :search-offset-top="120"
             />

+ 1 - 1
src/views/StatusMonitoring.vue

@@ -20,7 +20,7 @@
                 @map-crossing-mouseover="handleMapCrossingMouseover"
                 @map-crossing-mouseout="handleMapCrossingMouseout"
                 @bindTrunkMenuTree="handleTrunkMenuUpdate"
-                :show-search="true"
+                :show-search="false"
                 :search-offset-right="100"
                 :search-offset-top="120"
             />

+ 1 - 1
src/views/TrunkCoordination.vue

@@ -20,7 +20,7 @@
                 @map-crossing-mouseover="handleMapCrossingMouseover"
                 @map-crossing-mouseout="handleMapCrossingMouseout"
                 @bindTrunkMenuTree="handleTrunkMenuUpdate"
-                :show-search="true"
+                :show-search="false"
                 :search-offset-right="100"
                 :search-offset-top="120"
             />