<template>
  <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>
</template>

<script>
import Konva from 'konva';
import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
import { apiGetDetectorMonitorData } from '@/api';

// 检测器现挂在 armNode 内部，位置使用 arm-local 坐标（与 createArrowIcon 一致），
// y=-190 对应原摄像机锚点（路口前方约一倍车道处的虚拟检测门架），由 arm 旋转自动定位到四个方向。

// 方向箭头 SVG 路径映射
// 所有方向臂共用同一套图标（以N方向/向下驶入为基准），由 createRoadArm 的 rotation 自动旋转
// R（右转）用左转图标水平翻转
const arrowSvgMap = {
  S: require('@/assets/images/svg/icon_straight_down.svg'),
  L: require('@/assets/images/svg/icon_turn_down_left.svg'),
  U: require('@/assets/images/svg/icon_turn_down_left_uturn.svg'),
  R: require('@/assets/images/svg/icon_turn_down_left.svg'),  // 右转用左转图标，渲染时水平翻转
};

// SVG 原始文本映射（内联，避免 webpack loader 问题）
const svgRawCache = {};

// 首次加载时通过 Image → Canvas 获取不到 SVG 内容，所以直接用内联方式
// 从 require 得到的 URL（可能是 base64 或路径）加载图片
const imgCache = {};

function loadSvgImage(svgUrl, fillColor) {
  const cacheKey = svgUrl + '|' + fillColor;
  if (imgCache[cacheKey]) return Promise.resolve(imgCache[cacheKey]);

  // 如果是 base64 data URL，解码后替换颜色
  if (svgUrl.startsWith('data:image/svg+xml;base64,')) {
    const base64 = svgUrl.split(',')[1];
    let svgText = atob(base64);
    svgText = svgText.replace(/fill="#[0-9a-fA-F]{3,8}"/g, `fill="${fillColor}"`);
    const encoded = btoa(svgText);
    const dataUrl = 'data:image/svg+xml;base64,' + encoded;
    return new Promise(resolve => {
      const img = new Image();
      img.onload = () => { imgCache[cacheKey] = img; resolve(img); };
      img.src = dataUrl;
    });
  }

  // 普通 URL，fetch 后替换颜色
  return fetch(svgUrl)
    .then(r => r.text())
    .then(svgText => {
      const colored = svgText.replace(/fill="#[0-9a-fA-F]{3,8}"/g, `fill="${fillColor}"`);
      const blob = new Blob([colored], { type: 'image/svg+xml' });
      const url = URL.createObjectURL(blob);
      return new Promise(resolve => {
        const img = new Image();
        img.onload = () => { imgCache[cacheKey] = img; resolve(img); };
        img.src = url;
      });
    });
}

export default {
  name: 'IntersectionMapVideos',
  components: {
    SegmentedRadio,
  },
  inject: {
    // 用 default 兜底：IntersectionMapVideos 也可能被独立测试或其它非 DashboardLayout 父级使用
    dialogManager: { default: null },
  },
  props: {
    // 路口数字孪生数据
    mapData: {
      type: Object,
      required: true
    },
  },
  data() {
    return {
      stage: null,
      layer: null,
      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, 
        laneWidth: 40,
        halfRoad: 160,
        roadWidth: 320,
        armLength: 350
      },
      stageWidth: 900,  // 当前画布缩放后的真实宽度
      stageHeight: 900, // 当前画布缩放后的真实高度
      // 视频/检测器 切换
      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',
      };
    },
  },
  mounted() {
    this.initKonvaStage();
    if (this.mapData && Object.keys(this.mapData).length > 0) {
      this.renderStaticConfig();
      this.updateDynamicSignals();
    }
    this.initResizeObserver();
  },
  beforeDestroy() {
    this.stopDetectorPolling();
    this.closeDetectorDialog();
    this.closeAllCameraDialogs();
    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
    },
    displayMode() {
      this.applyDisplayMode();
    },
  },
  methods: {
    // ================= 以下为原有的 Konva 绘制逻辑，完全保持不变 =================
    initKonvaStage() {
      const { stageSize, halfRoad, roadWidth } = this.sizeConfig;
      const center = stageSize / 2;

      this.stage = new Konva.Stage({
        container: this.$refs.konvaContainer,
        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),
        S: this.createRoadArm(center, center + halfRoad, 180),
        W: this.createRoadArm(center - halfRoad, center, 270)
      };
      Object.values(this.armsNodes).forEach(arm => this.layer.add(arm));

      this.createCenterPanel(center);
      this.layer.draw();
    },

    initResizeObserver() {
      this.resizeObserver = new ResizeObserver(() => {
        window.requestAnimationFrame(() => {
          this.handleResize();
        });
      });
      if (this.$refs.wrapper) {
        this.resizeObserver.observe(this.$refs.wrapper);
      }
    },

    handleResize() {
      if (!this.stage || !this.$refs.wrapper) return;
      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.stageWidth = this.sizeConfig.stageSize * scale;
      this.stageHeight = this.sizeConfig.stageSize * scale;

      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));
      }

      // multi-view 布局变化（增删面板/expand 切换）会让 .detail-panel-right 重排，
      // 检测器弹窗若已开着，跟随重新定位+尺寸。重入 openDialog 走 existing 分支只更新位置尺寸。
      if (this.displayMode === 'detector') {
        this.openDetectorDialog();
      }
      // 已打开的摄像头视频弹窗按 2×2 网格重排（保留用户的下拉/拖拽不重置）
      this.repositionOpenCameraDialogs();
    },

    createRoadArm(x, y, rotation) {
      const { halfRoad, roadWidth, laneWidth } = this.sizeConfig;
      const group = new Konva.Group({ x, y, rotation });
      
      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 }));
      group.add(new Konva.Path({ data: `M 160 -350 L 160 -30 Q 160 0 180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
      group.add(new Konva.Line({ points: [-160, -35, 0, -35], stroke: this.C.WHITE, strokeWidth: 4 }));
      
      for (let i = 1; i < 4; i++) {
        let ox = i * laneWidth;
        group.add(new Konva.Line({ points: [-ox, -35, -ox, -120], stroke: this.C.WHITE, strokeWidth: 2 }));
        group.add(new Konva.Line({ points: [-ox, -120, -ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
        group.add(new Konva.Line({ points: [ox, -35, ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
      }
      
      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 }));
      for (let rx = 20; rx <= 148; rx += 16) lightGroup.add(new Konva.Rect({ x: rx, ...rectOpts }));
      group.add(lightGroup);
      group.lightGroup = lightGroup;

      group.arrowNodes = { 0: null, 1: null, 2: null, 3: null };
      group.cameraNode = null;

      return group;
    },

    createCenterPanel(center) {
      const panelGroup = new Konva.Group({ x: center - 80, y: center - 45 });
      panelGroup.add(new Konva.Rect({ width: 160, height: 90, fill: this.C.PANEL_BG, cornerRadius: 8 }));
      
      const labelFont = { fontSize: 18, fontFamily: 'monospace', fontStyle: 'bold', fill: this.C.WHITE }; 
      const valueFont = { fontSize: 28, fontFamily: 'monospace', fontStyle: 'bold' };              
      
      this.panelNodes.nsLabel = new Konva.Text({ ...labelFont, x: 15, y: 22, text: '相位-:' });
      this.panelNodes.nsVal = new Konva.Text({ ...valueFont, x: 90, y: 15, text: '--', fill: this.C.SIGNAL_GREEN });
      
      this.panelNodes.ewLabel = new Konva.Text({ ...labelFont, x: 15, y: 55, text: '相位-:' });
      this.panelNodes.ewVal = new Konva.Text({ ...valueFont, x: 90, y: 48, text: '--', fill: this.C.SIGNAL_GREEN });
      
      panelGroup.add(this.panelNodes.nsLabel, this.panelNodes.nsVal, this.panelNodes.ewLabel, this.panelNodes.ewVal);
      this.layer.add(panelGroup);
    },

    createArrowIcon(type, x, y, color = this.C.WHITE) {
      const maxH = 40; // 最大高度
      const group = new Konva.Group({ x, y });
      if (type === 'R') group.scaleX(-1);
      group._arrowMeta = { type };

      const svgUrl = arrowSvgMap[type];
      if (svgUrl) {
        loadSvgImage(svgUrl, color).then(imgObj => {
          // 按原始比例等比缩放，高度不超过 maxH
          const natW = imgObj.naturalWidth || imgObj.width;
          const natH = imgObj.naturalHeight || imgObj.height;
          const scale = Math.min(maxH / natH, 1);
          const w = Math.round(natW * scale);
          const h = Math.round(natH * scale);
          const konvaImg = new Konva.Image({
            image: imgObj,
            x: -w / 2,
            y: -h - 5,
            width: w,
            height: h,
            name: 'arrowImg'
          });
          group.add(konvaImg);
          if (this.layer) this.layer.draw();
        });
      }
      return group;
    },

    /** 检测器图标（球机造型）。放在进口道中线、徽章列上方；
     *  `armRotation` 用来反向旋转，让所有方向的图标都朝上，与 arm 旋转无关。 */
    createDetectorIcon(armRotation) {
      const stroke = '#7fb6ff';
      const group = new Konva.Group({
        x: -80, y: -240,
        rotation: -armRotation,
      });
      group.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)',
      }));
      group.add(new Konva.Circle({ x: 0, y: -16, radius: 7, stroke, strokeWidth: 2.5 }));
      group.add(new Konva.Line({ points: [-13, 0, 13, 0], stroke, strokeWidth: 2.5, lineCap: 'round' }));
      group.add(new Konva.Line({ points: [0, 0, 0, 8], stroke, strokeWidth: 2.5 }));
      return group;
    },

    /** 检测器编号徽章（小圆 + 数字）。文字反向旋转，保证四个方向都朝上。 */
    createDetectorBadge(lx, ly, num, armRotation) {
      const stroke = '#7fb6ff';
      const fill = '#3a7fd1';
      const group = new Konva.Group();
      group.add(new Konva.Circle({ x: lx, y: ly, radius: 13, fill, stroke, strokeWidth: 1.5 }));
      group.add(new Konva.Text({
        x: lx, y: ly,
        offsetX: 13, offsetY: 9, // 把旋转中心从左上角搬到 (lx, ly)
        width: 26, align: 'center',
        text: String(num), fontSize: 16, fontStyle: 'bold', fill: '#fff',
        rotation: -armRotation,
      }));
      return group;
    },

    /** (重)创建四个方向的检测器组：图标 + 每条车道一个编号徽章。
     *  组挂在 armNode 内，arm 旋转/平移自动定位；图标和数字通过反向旋转保持朝上。 */
    renderDetectors() {
      const cfg = (this.mapData && this.mapData.armsConfig) || {};
      const { laneWidth } = this.sizeConfig;
      const detY = -190; // 与原 cameraNode 锚点一致

      // 全路口连续编号：N→E→S→W 顺时针累加，每条车道一个号；
      // 每个方向内按司机视角"左→右"（arm-local 最外侧 → 最内侧）编号。
      let badgeNum = 1;

      ['N', 'E', 'S', 'W'].forEach(dir => {
        const armNode = this.armsNodes[dir];
        if (!armNode) return;

        // 清理旧检测器组
        if (armNode.detectorGroup) {
          armNode.detectorGroup.destroy();
          armNode.detectorGroup = null;
        }
        this.detectorNodes[dir] = null;

        const armData = cfg[dir];
        if (!armData) return;
        const lanes = armData.lanes || [];
        const armRotation = armNode.rotation() || 0;

        const detGroup = new Konva.Group();
        // 检测器图标（一方向一个，徽章列上方）
        detGroup.add(this.createDetectorIcon(armRotation));
        // 车道徽章
        for (let index = lanes.length - 1; index >= 0; index--) {
          const lx = -20 - index * laneWidth;
          detGroup.add(this.createDetectorBadge(lx, detY, badgeNum++, armRotation));
        }

        detGroup.visible(this.displayMode === 'detector');
        armNode.add(detGroup);
        armNode.detectorGroup = detGroup;
        this.detectorNodes[dir] = detGroup;
      });
      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;
      });
    },

    /** 用 detectorBase 当前值刷一次显示文本（当前画布版未绘制流量/占有率文字，
     *  保留方法以便未来恢复显示；polling 仍在跑，detectorBase 持续更新供 DetectorTable 等下游消费） */
    updateDetectorTexts() {
      ['N', 'E', 'S', 'W'].forEach(dir => {
        const base = this.detectorBase[dir];
        const node = this.detectorNodes[dir];
        if (!base || !node) return;
        if (node.flowText) node.flowText.text(`流量：${base.flow}`);
        if (node.occText) node.occText.text(`占有率：${base.occupancy}%`);
      });
      if (this.layer) this.layer.batchDraw();
    },

    /** 同源轮询：拉 apiGetDetectorMonitorData，把 armsDetector 写回 detectorBase 并刷新文字。
     *  与 DetectorTable 用同一接口，画布每 5s 跳到与表格同源的当前桶值。 */
    async pollDetectorData() {
      try {
        const id = this.getIntersectionId();
        const res = await apiGetDetectorMonitorData(id);
        const arms = res && res.armsDetector;
        if (!arms) return;
        ['N', 'E', 'S', 'W'].forEach(dir => {
          const d = arms[dir];
          if (d) this.detectorBase[dir] = { flow: d.flow, occupancy: d.occupancy };
        });
        this.updateDetectorTexts();
      } catch (e) {
        console.warn('[IntersectionMapVideos] poll detector failed:', e);
      }
    },

    startDetectorPolling() {
      if (this.detectorTimer) return;
      this.pollDetectorData();  // 进入检测器模式立即拉一次，避免空 5s 等待
      this.detectorTimer = setInterval(this.pollDetectorData, 5000);
    },

    stopDetectorPolling() {
      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.startDetectorPolling();
        this.openDetectorDialog();
        this.closeAllCameraDialogs();   // 切到检测器：关掉所有摄像头视频弹窗
      } else {
        this.stopDetectorPolling();
        this.closeDetectorDialog();     // 切回视频：关检测器弹窗（视频弹窗按需点开，无需自动开）
      }
    },

    /** 从 mapData 推导出当前路口 id（detectors / cameras 数组里都带 intersectionId） */
    getIntersectionId() {
      const m = this.mapData || {};
      if (Array.isArray(m.detectors) && m.detectors.length) return m.detectors[0].intersectionId || '';
      if (Array.isArray(m.cameras) && m.cameras.length) return m.cameras[0].intersectionId || '';
      return '';
    },

    /** 检测器模式下打开弹窗：默认贴到本路口详情面板右侧（控制方式区域）覆盖显示。
     *  无法定位时回退到居中默认尺寸。SmartDialog 用 1920 设计宽度做缩放，所以这里把
     *  实际像素 rect 换算回设计坐标传过去。 */
    openDetectorDialog() {
      if (!this.dialogManager || typeof this.dialogManager.openDialog !== 'function') return;

      const cfg = {
        id: `detector-monitor-${this._uid}`,
        title: '检测器运行数据监视',
        component: 'DetectorTable',
        noPadding: false,
        // 关闭右下角缩放手柄：弹窗尺寸由 .detail-panel-right rect 自动决定，
        // 用户手动缩放反而会破坏与父面板的尺寸联动。
        resizable: false,
        data: { intersectionId: this.getIntersectionId() },
      };

      const DESIGN_WIDTH = 1920;
      const scale = window.innerWidth / DESIGN_WIDTH;
      const root = this.$el && this.$el.closest && this.$el.closest('.crossing-detail-panel');
      const rightPanel = root && root.querySelector(':scope > .detail-panel-right');

      if (rightPanel && scale > 0) {
        const rect = rightPanel.getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0) {
          cfg.center = false;
          cfg.position = { x: rect.left / scale, y: rect.top / scale };
          cfg.width = Math.round(rect.width / scale);
          cfg.height = Math.round(rect.height / scale);
        }
      }
      // 兜底：找不到右侧面板（独立使用场景）则居中 620×360
      if (cfg.width == null) {
        cfg.width = 620;
        cfg.height = 360;
      }

      this.dialogManager.openDialog(cfg);
    },

    closeDetectorDialog() {
      if (!this.dialogManager || typeof this.dialogManager.closeDialog !== 'function') return;
      this.dialogManager.closeDialog(`detector-monitor-${this._uid}`);
    },

    /** 摄像头视频弹窗：每个方向一个独立弹窗（id 含 dir + _uid），最多 4 个并存。
     *  默认按 2×2 网格摆在 .detail-panel-right 区域内（N→左上 / E→右上 / W→左下 / S→右下），
     *  跟随 multi-view 布局变化重排，但用户拖拽/缩放/下拉切换都保留不重置。 */
    openCameraDialog(dir) {
      if (!this.dialogManager || typeof this.dialogManager.openDialog !== 'function') return;

      const DIR_LABEL = { N: '北进口', E: '东进口', S: '南进口', W: '西进口' };
      const cfg = {
        id: `camera-video-${this._uid}-${dir}`,
        title: `摄像头-${DIR_LABEL[dir] || dir}`,
        component: 'CameraVideoDialog',
        noPadding: true,   // 关掉 SmartDialog 默认 padding，组件内自管
        resizable: true,
        draggable: true,
        // 调小弹窗最小尺寸，allow multi-view 4 宫格里的小弹窗不被卡死在 200×150
        minWidth: 80,
        minHeight: 60,
        data: {
          intersectionId: this.getIntersectionId(),
          initialDir: dir,
          cameras: (this.mapData && this.mapData.cameras) || [],
        },
      };

      const rect = this._calcCameraDialogRect(dir);
      if (rect) {
        cfg.center = false;
        cfg.position = rect.position;
        cfg.width = rect.width;
        cfg.height = rect.height;
      } else {
        cfg.width = 380;
        cfg.height = 280;
      }

      this.dialogManager.openDialog(cfg);
    },

    closeAllCameraDialogs() {
      if (!this.dialogManager || typeof this.dialogManager.closeDialog !== 'function') return;
      ['N', 'E', 'S', 'W'].forEach(dir => {
        this.dialogManager.closeDialog(`camera-video-${this._uid}-${dir}`);
      });
    },

    /** 仅更新位置/尺寸（不传 data），避免重置用户在弹窗内的下拉选择 */
    repositionOpenCameraDialogs() {
      if (!this.dialogManager || typeof this.dialogManager.getDialogs !== 'function') return;
      const dialogs = this.dialogManager.getDialogs();
      const prefix = `camera-video-${this._uid}-`;
      dialogs.forEach(d => {
        const idStr = String(d.id);
        if (!idStr.startsWith(prefix) || !d.visible) return;
        const dir = idStr.slice(prefix.length);
        const rect = this._calcCameraDialogRect(dir);
        if (!rect) return;
        this.dialogManager.openDialog({
          id: idStr,
          center: false,
          position: rect.position,
          width: rect.width,
          height: rect.height,
        });
      });
    },

    /** 把 dir 映射成 .detail-panel-right 内的 2×2 单元格，返回设计坐标系下的 position/width/height。
     *  N→左上, E→右上, W→左下, S→右下，与画面方位一致。 */
    _calcCameraDialogRect(dir) {
      const DESIGN_WIDTH = 1920;
      const scale = window.innerWidth / DESIGN_WIDTH;
      const root = this.$el && this.$el.closest && this.$el.closest('.crossing-detail-panel');
      const rightPanel = root && root.querySelector(':scope > .detail-panel-right');
      if (!(rightPanel && scale > 0)) return null;
      const rect = rightPanel.getBoundingClientRect();
      if (!(rect.width > 0 && rect.height > 0)) return null;

      const DIR_TO_CELL = {
        N: { col: 0, row: 0 },
        E: { col: 1, row: 0 },
        W: { col: 0, row: 1 },
        S: { col: 1, row: 1 },
      };
      const GAP = 4;       // 单元格间距（设计像素）

      // 单元尺寸严格按右侧面板的 1/2 切分，不加最小兜底
      // —— 多窗口下面板可能只剩 ~300×200 设计像素，加 MIN 反而让 4 弹窗溢出
      const wDesign = rect.width / scale;
      const hDesign = rect.height / scale;
      const cellW = Math.max(40, Math.floor((wDesign - GAP) / 2));
      const cellH = Math.max(40, Math.floor((hDesign - GAP) / 2));
      const cell = DIR_TO_CELL[dir] || { col: 0, row: 0 };
      return {
        position: {
          x: rect.left / scale + cell.col * (cellW + GAP),
          y: rect.top / scale + cell.row * (cellH + GAP),
        },
        width: cellW,
        height: cellH,
      };
    },

    createCameraIcon(type, x, y, dir, armRotation) {
      // 反向旋转：让 4 个方向的摄像头图标都正立显示（与检测器图标一致），
      // 否则 E/S/W 会随 arm 旋转 90/180/270，倒立或横向的 枪机 视觉上像球机/检测器。
      const group = new Konva.Group({ x, y, rotation: -armRotation });
      const HIT = 18; // 加大点击命中区，避免只有线条上能点

      if (type === 1) {
        group.add(new Konva.Line({ points: [-16, -30, 16, -30], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round', hitStrokeWidth: HIT }));
        group.add(new Konva.Line({ points: [0, -30, 0, -10], stroke: this.C.BLUE, strokeWidth: 2.5, hitStrokeWidth: HIT }));
        const body = new Konva.Group({ y: -10, rotation: 15 });
        body.add(new Konva.Rect({ x: -16, y: -8, width: 32, height: 16, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 2, hitStrokeWidth: HIT }));
        body.add(new Konva.Rect({ x: 16, y: -4, width: 6, height: 8, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 1, hitStrokeWidth: HIT }));
        group.add(body);
      } else if (type === 2) {
        group.add(new Konva.Rect({ x: -14, y: -24, width: 28, height: 24, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 6, hitStrokeWidth: HIT }));
        group.add(new Konva.Circle({ x: 0, y: -12, radius: 5, stroke: this.C.BLUE, strokeWidth: 2.5, hitStrokeWidth: HIT }));
        group.add(new Konva.Line({ points: [-10, 0, 10, 0], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round', hitStrokeWidth: HIT }));
        group.add(new Konva.Line({ points: [0, 0, 0, 6], stroke: this.C.BLUE, strokeWidth: 2.5, hitStrokeWidth: HIT }));
      }

      group.listening(true);
      const stage = () => this.stage && this.stage.container();
      group.on('mouseenter', () => { const c = stage(); if (c) c.style.cursor = 'pointer'; });
      group.on('mouseleave', () => { const c = stage(); if (c) c.style.cursor = 'default'; });
      group.on('click tap', () => this.openCameraDialog(dir));
      return group;
    },

    renderStaticConfig() {
      const config = this.mapData.armsConfig;
      if (!config) return;

      Object.keys(config).forEach(dir => {
        const armData = config[dir];
        const armNode = this.armsNodes[dir];

        if (armNode.cameraNode) armNode.cameraNode.destroy();
        if (armData.cameraType > 0) {
          const armRotation = armNode.rotation() || 0;
          const cam = this.createCameraIcon(armData.cameraType, -80, -190, dir, armRotation);
          armNode.add(cam);
          armNode.cameraNode = cam;
        }

        armData.lanes.forEach((type, index) => {
          if (armNode.arrowNodes[index]) armNode.arrowNodes[index].destroy();
          if (type) {
            const lx = -20 - (index * this.sizeConfig.laneWidth);
            const arrow = this.createArrowIcon(type, lx, -80, this.C.WHITE);
            armNode.add(arrow);
            armNode.arrowNodes[index] = arrow;
          }
        });
      });
      this.renderDetectors();
      this.applyDisplayMode();
      this.layer.draw();
    },

    updateDynamicSignals() {
      const signals = this.mapData.signals;
      if (!signals) return;
      const config = this.mapData.armsConfig || {};

      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 nsActiveTypes = signals.ns.activeArrowTypes || [];
      const ewActiveTypes = signals.ew.activeArrowTypes || [];

      const dyeArm = (dir, armNode, pedColor, vehicleColor, activeTypes) => {
        // 灯带颜色（人行道信号）
        armNode.lightGroup.getChildren().forEach(r => r.fill(pedColor));
        // 箭头按 lane type 用不同颜色的 SVG 替换
        const lanes = (config[dir] && config[dir].lanes) || [];
        Object.keys(armNode.arrowNodes).forEach(index => {
          const arr = armNode.arrowNodes[index];
          if (!arr) return;
          const laneType = lanes[index];
          const isActive = activeTypes.length > 0 && activeTypes.includes(laneType);
          const targetColor = isActive ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;

          const meta = arr._arrowMeta;
          if (!meta) return;
          const svgUrl = arrowSvgMap[meta.type];
          if (!svgUrl) return;

          loadSvgImage(svgUrl, targetColor).then(imgObj => {
            const existing = arr.findOne('.arrowImg');
            if (existing) {
              existing.image(imgObj);
            } else {
              const maxH = 40;
              const natW = imgObj.naturalWidth || imgObj.width;
              const natH = imgObj.naturalHeight || imgObj.height;
              const scale = Math.min(maxH / natH, 1);
              const w = Math.round(natW * scale);
              const h = Math.round(natH * scale);
              arr.add(new Konva.Image({
                image: imgObj,
                x: -w / 2,
                y: -h - 5,
                width: w,
                height: h,
                name: 'arrowImg'
              }));
            }
            if (this.layer) this.layer.draw();
          });
        });
      };

      // 灯带代表人行道：P1/P3绿灯期间正常（车绿人红、车红人绿），其余时段人行道全红
      const pedAllRed = signals.pedAllRed || false;
      const nsPedColor = pedAllRed ? this.C.SIGNAL_RED : (signals.ns.isGreen ? this.C.SIGNAL_RED : this.C.SIGNAL_GREEN);
      const ewPedColor = pedAllRed ? this.C.SIGNAL_RED : (signals.ew.isGreen ? this.C.SIGNAL_RED : this.C.SIGNAL_GREEN);

      dyeArm('N', this.armsNodes.N, nsPedColor, nsColor, nsActiveTypes);
      dyeArm('S', this.armsNodes.S, nsPedColor, nsColor, nsActiveTypes);
      dyeArm('E', this.armsNodes.E, ewPedColor, ewColor, ewActiveTypes);
      dyeArm('W', this.armsNodes.W, ewPedColor, ewColor, ewActiveTypes);

      this.panelNodes.nsLabel.text(`${signals.ns.phaseName}:`);
      this.panelNodes.nsVal.text(signals.ns.time.toString().padStart(2, '0')).fill(nsColor);

      this.panelNodes.ewLabel.text(`${signals.ew.phaseName}:`);
      this.panelNodes.ewVal.text(signals.ew.time.toString().padStart(2, '0')).fill(ewColor);

      this.layer.draw();
    }
  }
};
</script>

<style scoped>
/* ================= 地图外层 ================= */
.map-wrapper {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: #212842;
  position: relative; /* 核心：让子元素能在其内部绝对定位 */
}

.konva-container {
  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;
}
</style>