<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from "vue";

type DeviceStatus = "空闲" | "离线" | "充电中";
type PatrolPointType = "业务点" | "途经点";
type PointDirection = "正向" | "反向";
type PatrolTaskMode = "定时" | "周期" | "临时";
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint";
interface DeviceRecord {
  id: string;
  name: string;
  battery: string;
  status: DeviceStatus;
  project: string;
  gimbalSerial: string;
  whiteLightUrl?: string;
  infraredUrl?: string;
  createdAt: string;
}

interface DeviceFormState {
  id: string;
  name: string;
  gimbalSerial: string;
  whiteLightUrl: string;
  infraredUrl: string;
}

interface MapRecord {
  id: string;
  name: string;
  version: string;
  coverage: string;
  updatedAt: string;
  pointCloudCount: number;
}

interface PatrolPoint {
  id: string;
  planId: string;
  name: string;
  type: PatrolPointType;
  direction: PointDirection;
  x: number;
  y: number;
  routeSpeed: string;
  stopDuration: string;
  liftHeight: string;
  gimbalPreset: string;
  note: string;
}

interface PatrolTask {
  id: string;
  name: string;
  mode: PatrolTaskMode;
  time: string;
  cycle: string;
  routeRange: string;
  enabled: boolean;
  color: string;
  loopCount: number;
  returnMode: string;
  speed: string;
  stay: string;
  rule: string;
  updatedAt: string;
}

interface DeviceConfigState {
  maps: MapRecord[];
  activeMapId: string;
  activePlanId: string;
  mapLoaded: boolean;
  mapSyncing: boolean;
  syncProgress: number;
  lastSyncedAt: string;
  points: PatrolPoint[];
  selectedPointIds: string[];
  plans: PatrolTask[];
}

interface PlanFormState {
  roundName: string;
  pointName: string;
  pointType: PatrolPointType;
  pointDirection: PointDirection;
  liftHeight: string;
  gimbalPreset: string;
}

interface BatchFormState {
  routeSpeed: string;
  stopDuration: string;
  liftHeight: string;
  gimbalPreset: string;
}

const defaultProject = "西江隧道";
const pointTypeOptions: PatrolPointType[] = ["业务点", "途经点"];
const pointDirectionOptions: PointDirection[] = ["正向", "反向"];

const editorModes: Array<{ id: EditorMode; label: string }> = [
  { id: "normal", label: "新增业务点" },
  { id: "waypoint", label: "新增途经点" },
];

const devices = ref<DeviceRecord[]>([
  {
    id: "ROBOT-001",
    name: "巡检机器人 R1",
    battery: "82%",
    status: "空闲",
    project: "西江隧道",
    gimbalSerial: "YPT-R1-24001",
    createdAt: "2026-04-11 09:30:22",
  },
  {
    id: "ROBOT-002",
    name: "巡检机器人 R2",
    battery: "76%",
    status: "空闲",
    project: "西江隧道",
    gimbalSerial: "YPT-R2-24002",
    createdAt: "2026-04-12 13:18:45",
  },
  {
    id: "ROBOT-003",
    name: "巡检机器人 R3",
    battery: "48%",
    status: "充电中",
    project: "西江隧道",
    gimbalSerial: "YPT-R3-24003",
    createdAt: "2026-04-15 10:42:10",
  },
  {
    id: "ROBOT-004",
    name: "巡检机器人 R4",
    battery: "91%",
    status: "空闲",
    project: "西江隧道",
    gimbalSerial: "YPT-R4-24004",
    createdAt: "2026-04-18 08:25:37",
  },
  {
    id: "ROBOT-101",
    name: "巡检机器人 Q1",
    battery: "88%",
    status: "空闲",
    project: "青云隧道",
    gimbalSerial: "YPT-Q1-24101",
    createdAt: "2026-04-19 14:06:51",
  },
  {
    id: "ROBOT-102",
    name: "巡检机器人 Q2",
    battery: "12%",
    status: "离线",
    project: "青云隧道",
    gimbalSerial: "YPT-Q2-24102",
    createdAt: "2026-04-21 11:09:26",
  },
  {
    id: "ROBOT-103",
    name: "巡检机器人 Q3",
    battery: "67%",
    status: "空闲",
    project: "青云隧道",
    gimbalSerial: "YPT-Q3-24103",
    createdAt: "2026-04-23 16:33:12",
  },
  {
    id: "ROBOT-104",
    name: "巡检机器人 Q4",
    battery: "39%",
    status: "充电中",
    project: "青云隧道",
    gimbalSerial: "YPT-Q4-24104",
    createdAt: "2026-04-25 09:56:08",
  },
  {
    id: "ROBOT-201",
    name: "巡检机器人 N1",
    battery: "95%",
    status: "空闲",
    project: "南山隧道",
    gimbalSerial: "YPT-N1-24201",
    createdAt: "2026-04-27 15:20:49",
  },
  {
    id: "ROBOT-202",
    name: "巡检机器人 N2",
    battery: "73%",
    status: "空闲",
    project: "南山隧道",
    gimbalSerial: "YPT-N2-24202",
    createdAt: "2026-04-28 10:14:33",
  },
  {
    id: "ROBOT-203",
    name: "巡检机器人 N3",
    battery: "64%",
    status: "空闲",
    project: "南山隧道",
    gimbalSerial: "YPT-N3-24203",
    createdAt: "2026-05-02 17:08:54",
  },
  {
    id: "ROBOT-204",
    name: "巡检机器人 N4",
    battery: "44%",
    status: "充电中",
    project: "南山隧道",
    gimbalSerial: "YPT-N4-24204",
    createdAt: "2026-05-05 08:48:19",
  },
]);

const pageSize = 6;
const currentPage = ref(1);
const keyword = ref("");
const dialogVisible = ref(false);
const configDialogVisible = ref(false);
const planDialogVisible = ref(false);
const saveProgressVisible = ref(false);
const saveProgressValue = ref(0);
const saveProgressMessage = ref("数据正在保存中...");
const editingId = ref<string | null>(null);
const configuringDeviceId = ref<string | null>(null);
const editingPointId = ref<string | null>(null);
const formError = ref("");
const mapEditorMode = ref<EditorMode>("select");
const mapStageRef = ref<HTMLElement | null>(null);
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
const suppressStageClick = ref(false);

const form = reactive<DeviceFormState>({
  id: "",
  name: "",
  gimbalSerial: "",
  whiteLightUrl: "",
  infraredUrl: "",
});

const planForm = reactive<PlanFormState>({
  roundName: "",
  pointName: "",
  pointType: "业务点",
  pointDirection: "正向",
  liftHeight: "1500",
  gimbalPreset: "预置点1",
});

const batchForm = reactive<BatchFormState>({
  routeSpeed: "",
  stopDuration: "",
  liftHeight: "",
  gimbalPreset: "",
});

const selectionBox = reactive({
  active: false,
  moved: false,
  startX: 0,
  startY: 0,
  endX: 0,
  endY: 0,
});

const dragPointState = reactive({
  active: false,
  moved: false,
  pointId: "",
  offsetX: 0,
  offsetY: 0,
});

let syncTimer: number | null = null;
let mapRenderFrame: number | null = null;
let mapResizeObserver: ResizeObserver | null = null;
let mapPointerTrackingBound = false;
let saveProgressTimer: number | null = null;
let saveProgressCloseTimer: number | null = null;

const mapPointPalette: Record<
  PatrolPointType,
  {
    stroke: string;
    glow: string;
    labelBackground: string;
    labelBorder: string;
  }
> = {
  业务点: {
    stroke: "#66d7ff",
    glow: "rgba(102, 215, 255, 0.28)",
    labelBackground: "rgba(16, 74, 118, 0.92)",
    labelBorder: "rgba(91, 208, 255, 0.18)",
  },
  途经点: {
    stroke: "#9af7c8",
    glow: "rgba(154, 247, 200, 0.3)",
    labelBackground: "rgba(18, 90, 66, 0.94)",
    labelBorder: "rgba(91, 208, 255, 0.18)",
  },
};

const selectedPointHighlight = {
  stroke: "#ffb26e",
  glow: "rgba(255, 178, 110, 0.32)",
  labelBackground: "rgba(122, 72, 18, 0.94)",
  labelBorder: "rgba(255, 196, 118, 0.72)",
};

const pointCloudSamples = Array.from({ length: 48 }, (_, index) => ({
  x: 8 + ((index * 1.9) % 86),
  y: 18 + ((index * 7.3) % 48),
  radius: index % 3 === 0 ? 0.45 : 0.3,
}));

function nowString() {
  return new Date().toLocaleString("zh-CN", { hour12: false });
}

function buildDeviceMaps(project: string): MapRecord[] {
  return [
    {
      id: `${project}-main`,
      name: `${project} 主通道环境地图`,
      version: "v3.4",
      coverage: "0m - 2200m",
      updatedAt: "2026-06-25 21:40:18",
      pointCloudCount: 1648,
    },
    {
      id: `${project}-equipment`,
      name: `${project} 设备区补充地图`,
      version: "v1.8",
      coverage: "泵房 / 风机 / 配电间",
      updatedAt: "2026-06-24 17:16:05",
      pointCloudCount: 932,
    },
  ];
}

function buildDevicePoints(device: DeviceRecord): PatrolPoint[] {
  return [
    {
      id: `${device.id}-P01`,
      planId: `${device.id}-TASK01`,
      name: `${device.project} 入口巡航点`,
      type: "业务点",
      direction: "正向",
      x: 12,
      y: 62,
      routeSpeed: "0.42",
      stopDuration: "8",
      liftHeight: "1350",
      gimbalPreset: "预置点1",
      note: "入口洞门全景与车道观察",
    },
    {
      id: `${device.id}-P02`,
      planId: `${device.id}-TASK01`,
      name: `${device.project} 热点复核点`,
      type: "业务点",
      direction: "反向",
      x: 38,
      y: 44,
      routeSpeed: "0.28",
      stopDuration: "12",
      liftHeight: "1680",
      gimbalPreset: "预置点2",
      note: "热成像复核桥架及电缆温升",
    },
    {
      id: `${device.id}-P03`,
      planId: `${device.id}-TASK02`,
      name: `${device.project} 泵房途经点`,
      type: "途经点",
      direction: "正向",
      x: 66,
      y: 56,
      routeSpeed: "0.24",
      stopDuration: "18",
      liftHeight: "1820",
      gimbalPreset: "预置点3",
      note: "停留采集泵房铭牌与压力表",
    },
    {
      id: `${device.id}-P04`,
      planId: `${device.id}-TASK02`,
      name: `${device.project} 终点回库点`,
      type: "业务点",
      direction: "反向",
      x: 84,
      y: 34,
      routeSpeed: "0.35",
      stopDuration: "6",
      liftHeight: "1260",
      gimbalPreset: "预置点4",
      note: "回库前执行末端巡检确认",
    },
  ];
}

function buildDevicePlans(device: DeviceRecord): PatrolTask[] {
  return [
    {
      id: `${device.id}-TASK01`,
      name: `${device.project} 日常定时巡检`,
      mode: "定时",
      time: "02:00",
      cycle: "每日",
      routeRange: "入口至中段",
      enabled: true,
      color: "#5ce1e6",
      loopCount: 1,
      returnMode: "完成后原路返回",
      speed: "0.42",
      stay: "8",
      rule: "工作日每日执行，优先覆盖主通道结构点位。",
      updatedAt: "2026-06-26 08:10:00",
    },
    {
      id: `${device.id}-TASK02`,
      name: `${device.project} 夜间设备复核`,
      mode: "周期",
      time: "22:30",
      cycle: "每2小时",
      routeRange: "泵房 / 配电区",
      enabled: true,
      color: "#f7bf5a",
      loopCount: 2,
      returnMode: "结束后前往充电桩",
      speed: "0.28",
      stay: "15",
      rule: "重点巡查机电设备区，异常时自动追加复核。",
      updatedAt: "2026-06-25 18:24:36",
    },
  ];
}

function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
  const maps = buildDeviceMaps(device.project);
  const points = buildDevicePoints(device);

  return {
    maps,
    activeMapId: maps[0].id,
    activePlanId: `${device.id}-TASK01`,
    mapLoaded: true,
    mapSyncing: false,
    syncProgress: 100,
    lastSyncedAt: "2026-06-26 09:15:08",
    points,
    selectedPointIds: [points[0].id],
    plans: buildDevicePlans(device),
  };
}

const deviceConfigs = reactive<Record<string, DeviceConfigState>>(
  Object.fromEntries(
    devices.value.map((device) => [device.id, buildDeviceConfig(device)]),
  ),
);

const filteredDevices = computed(() => {
  const search = keyword.value.trim().toLowerCase();

  return devices.value.filter((device) => {
    const matchKeyword =
      !search ||
      [device.id, device.name, device.gimbalSerial]
        .join(" ")
        .toLowerCase()
        .includes(search);

    return matchKeyword;
  });
});

const totalPages = computed(() =>
  Math.max(1, Math.ceil(filteredDevices.value.length / pageSize)),
);

const pagedDevices = computed(() => {
  const start = (currentPage.value - 1) * pageSize;
  return filteredDevices.value.slice(start, start + pageSize);
});

const visiblePages = computed(() => {
  const pages: number[] = [];
  const start = Math.max(1, currentPage.value - 2);
  const end = Math.min(totalPages.value, start + 4);

  for (let page = start; page <= end; page += 1) {
    pages.push(page);
  }

  return pages;
});

const currentConfiguredDevice = computed(
  () =>
    devices.value.find((device) => device.id === configuringDeviceId.value) ??
    null,
);

const currentConfig = computed(() =>
  configuringDeviceId.value
    ? (deviceConfigs[configuringDeviceId.value] ?? null)
    : null,
);

const activeMap = computed(
  () =>
    currentConfig.value?.maps.find(
      (map) => map.id === currentConfig.value?.activeMapId,
    ) ?? null,
);

const activeRound = computed(
  () =>
    currentConfig.value?.plans.find(
      (plan) => plan.id === currentConfig.value?.activePlanId,
    ) ?? null,
);

const activeRoundPoints = computed(() => {
  if (!currentConfig.value) {
    return [];
  }

  return currentConfig.value.points;
});

const selectedPoints = computed(() => {
  if (!currentConfig.value) {
    return [];
  }

  return activeRoundPoints.value.filter((point) =>
    currentConfig.value?.selectedPointIds.includes(point.id),
  );
});

const singleSelectedPoint = computed(() =>
  selectedPoints.value.length === 1 ? selectedPoints.value[0] : null,
);

function resetForm() {
  form.id = "";
  form.name = "";
  form.gimbalSerial = "";
  form.whiteLightUrl = "";
  form.infraredUrl = "";
  formError.value = "";
}

function resetPlanForm() {
  editingPointId.value = null;
  planForm.roundName = "";
  planForm.pointName = "";
  planForm.pointType = "业务点";
  planForm.pointDirection = "正向";
  planForm.liftHeight = "1500";
  planForm.gimbalPreset = "预置点1";
}

function openCreateDialog() {
  editingId.value = null;
  resetForm();
  dialogVisible.value = true;
}

function openEditDialog(device: DeviceRecord) {
  editingId.value = device.id;
  form.id = device.id;
  form.name = device.name;
  form.gimbalSerial = device.gimbalSerial;
  form.whiteLightUrl = device.whiteLightUrl ?? "";
  form.infraredUrl = device.infraredUrl ?? "";
  formError.value = "";
  dialogVisible.value = true;
}

function openConfigDialog(device: DeviceRecord) {
  const config = deviceConfigs[device.id] ?? buildDeviceConfig(device);

  deviceConfigs[device.id] = config;
  configuringDeviceId.value = device.id;
  configDialogVisible.value = true;
  mapEditorMode.value = "normal";

  if (!config.plans.some((plan) => plan.id === config.activePlanId)) {
    config.activePlanId = config.plans[0]?.id ?? "";
  }

  const currentRoundPoints = config.points;

  if (!config.selectedPointIds.length && currentRoundPoints.length) {
    config.selectedPointIds = [currentRoundPoints[0].id];
  }

  syncBatchForm();
  resetPlanForm();
  nextTick(() => {
    attachMapResizeObserver();
    scheduleMapCanvasRender();
  });
}

function closeDialog() {
  dialogVisible.value = false;
  formError.value = "";
}

function closeConfigDialog() {
  if (saveProgressVisible.value) {
    return;
  }

  configDialogVisible.value = false;
  planDialogVisible.value = false;
  configuringDeviceId.value = null;
  clearSelectionBox();
  clearDragPointState();
  stopMapSyncTimer();
}

function stopSaveProgressTimers() {
  if (saveProgressTimer !== null) {
    window.clearInterval(saveProgressTimer);
    saveProgressTimer = null;
  }

  if (saveProgressCloseTimer !== null) {
    window.clearTimeout(saveProgressCloseTimer);
    saveProgressCloseTimer = null;
  }
}

function finishSaveProgress() {
  stopSaveProgressTimers();
  saveProgressValue.value = 100;
  saveProgressMessage.value = "保存成功，正在关闭...";

  saveProgressCloseTimer = window.setTimeout(() => {
    saveProgressVisible.value = false;
    saveProgressCloseTimer = null;
    configDialogVisible.value = false;
    planDialogVisible.value = false;
    configuringDeviceId.value = null;
    clearSelectionBox();
    clearDragPointState();
    stopMapSyncTimer();
  }, 700);
}

function setActiveRound(planId: string) {
  if (!currentConfig.value) {
    return;
  }

  currentConfig.value.activePlanId = planId;

  if (!currentConfig.value.selectedPointIds.length && currentConfig.value.points.length) {
    updateSelection([currentConfig.value.points[0].id]);
  }
}

function buildNextId() {
  const maxId = devices.value.reduce((maxValue, device) => {
    const numericPart = Number(device.id.replace(/\D/g, ""));
    return Number.isNaN(numericPart)
      ? maxValue
      : Math.max(maxValue, numericPart);
  }, 0);

  return `ROBOT-${String(maxId + 1).padStart(3, "0")}`;
}

function saveDevice() {
  const existingDevice = editingId.value
    ? devices.value.find((device) => device.id === editingId.value)
    : null;

  if (!form.name.trim()) {
    formError.value = "请填写设备名称";
    return;
  }

  if (!form.gimbalSerial.trim()) {
    formError.value = "请填写云台序列号";
    return;
  }

  const payload: DeviceRecord = {
    id: editingId.value ?? buildNextId(),
    name: form.name.trim(),
    battery: existingDevice?.battery ?? "--",
    status: existingDevice?.status ?? "离线",
    project: existingDevice?.project ?? defaultProject,
    gimbalSerial: form.gimbalSerial.trim(),
    whiteLightUrl: form.whiteLightUrl.trim(),
    infraredUrl: form.infraredUrl.trim(),
    createdAt: existingDevice?.createdAt ?? nowString(),
  };

  if (editingId.value) {
    devices.value = devices.value.map((device) =>
      device.id === editingId.value ? payload : device,
    );
  } else {
    devices.value = [payload, ...devices.value];
    deviceConfigs[payload.id] = buildDeviceConfig(payload);
  }

  closeDialog();
  currentPage.value = 1;
}

function removeDevice(id: string) {
  if (!window.confirm("确认删除该设备吗？")) {
    return;
  }

  devices.value = devices.value.filter((device) => device.id !== id);
  delete deviceConfigs[id];

  if (configuringDeviceId.value === id) {
    closeConfigDialog();
  }

  if (currentPage.value > totalPages.value) {
    currentPage.value = totalPages.value;
  }
}

function setPage(page: number) {
  currentPage.value = page;
}

function handleFilterChange() {
  currentPage.value = 1;
}

function getStatusClass(status: DeviceStatus) {
  const classMap: Record<DeviceStatus, string> = {
    空闲: "status-idle",
    离线: "status-offline",
    充电中: "status-charging",
  };

  return classMap[status];
}

function getMapStageRect() {
  return mapStageRef.value?.getBoundingClientRect() ?? null;
}

function percentToStagePixels(x: number, y: number, rect: DOMRect) {
  return {
    x: (x / 100) * rect.width,
    y: (y / 100) * rect.height,
  };
}

function estimatePointLabelWidth(name: string) {
  return Math.max(72, name.length * 12 + 18);
}

function getPointLabelFrame(point: PatrolPoint, rect: DOMRect) {
  const pixelPoint = percentToStagePixels(point.x, point.y, rect);
  const labelWidth = estimatePointLabelWidth(point.name);
  const labelHeight = 24;
  let left = pixelPoint.x + 18;

  if (left + labelWidth > rect.width - 10) {
    left = pixelPoint.x - labelWidth - 18;
  }

  return {
    left,
    top: pixelPoint.y - labelHeight / 2,
    width: labelWidth,
    height: labelHeight,
  };
}

function findHitPoint(position: { x: number; y: number }) {
  const rect = getMapStageRect();
  if (!rect) {
    return null;
  }

  const points = [...activeRoundPoints.value].reverse();

  for (const point of points) {
    const pixelPoint = percentToStagePixels(point.x, point.y, rect);
    const deltaX = pixelPoint.x - (position.x / 100) * rect.width;
    const deltaY = pixelPoint.y - (position.y / 100) * rect.height;

    if (Math.hypot(deltaX, deltaY) <= 16) {
      return point;
    }

    const labelFrame = getPointLabelFrame(point, rect);
    const pointerX = (position.x / 100) * rect.width;
    const pointerY = (position.y / 100) * rect.height;

    if (
      pointerX >= labelFrame.left &&
      pointerX <= labelFrame.left + labelFrame.width &&
      pointerY >= labelFrame.top &&
      pointerY <= labelFrame.top + labelFrame.height
    ) {
      return point;
    }
  }

  return null;
}

function drawRoundRect(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  width: number,
  height: number,
  radius: number,
) {
  const clampedRadius = Math.min(radius, width / 2, height / 2);

  ctx.beginPath();
  ctx.moveTo(x + clampedRadius, y);
  ctx.lineTo(x + width - clampedRadius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + clampedRadius);
  ctx.lineTo(x + width, y + height - clampedRadius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - clampedRadius, y + height);
  ctx.lineTo(x + clampedRadius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - clampedRadius);
  ctx.lineTo(x, y + clampedRadius);
  ctx.quadraticCurveTo(x, y, x + clampedRadius, y);
  ctx.closePath();
}

function renderMapCanvas() {
  const canvas = mapCanvasRef.value;
  const rect = getMapStageRect();

  if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) {
    return;
  }

  const context = canvas.getContext("2d");
  if (!context) {
    return;
  }

  const dpr = window.devicePixelRatio || 1;
  const displayWidth = Math.max(1, Math.round(rect.width));
  const displayHeight = Math.max(1, Math.round(rect.height));

  if (
    canvas.width !== Math.round(displayWidth * dpr) ||
    canvas.height !== Math.round(displayHeight * dpr)
  ) {
    canvas.width = Math.round(displayWidth * dpr);
    canvas.height = Math.round(displayHeight * dpr);
  }

  context.setTransform(dpr, 0, 0, dpr, 0, 0);
  context.clearRect(0, 0, displayWidth, displayHeight);

  const unitX = displayWidth / 100;
  const unitY = displayHeight / 100;

  context.save();
  context.strokeStyle = "rgba(97, 214, 255, 0.08)";
  context.lineWidth = 1;

  for (let line = 1; line < 10; line += 1) {
    context.beginPath();
    context.moveTo(0, line * 10 * unitY);
    context.lineTo(displayWidth, line * 10 * unitY);
    context.stroke();

    context.beginPath();
    context.moveTo(line * 10 * unitX, 0);
    context.lineTo(line * 10 * unitX, displayHeight);
    context.stroke();
  }
  context.restore();

  context.save();
  context.fillStyle = "rgba(146, 236, 255, 0.42)";
  pointCloudSamples.forEach((sample) => {
    context.beginPath();
    context.arc(sample.x * unitX, sample.y * unitY, sample.radius * unitX, 0, Math.PI * 2);
    context.fill();
  });
  context.restore();

  context.save();
  context.beginPath();
  context.moveTo(6 * unitX, 72 * unitY);
  context.bezierCurveTo(16 * unitX, 68 * unitY, 28 * unitX, 50 * unitY, 40 * unitX, 47 * unitY);
  context.bezierCurveTo(52 * unitX, 44 * unitY, 62 * unitX, 62 * unitY, 76 * unitX, 52 * unitY);
  context.bezierCurveTo(86 * unitX, 46 * unitY, 92 * unitX, 32 * unitY, 96 * unitX, 28 * unitY);
  context.lineCap = "round";
  context.strokeStyle = "rgba(78, 192, 255, 0.12)";
  context.lineWidth = 18;
  context.stroke();
  context.strokeStyle = "rgba(78, 192, 255, 0.28)";
  context.lineWidth = 8;
  context.stroke();
  context.restore();

  if (activeRoundPoints.value.length > 1) {
    context.save();
    context.beginPath();
    activeRoundPoints.value.forEach((point, index) => {
      const pixelPoint = percentToStagePixels(point.x, point.y, rect);

      if (index === 0) {
        context.moveTo(pixelPoint.x, pixelPoint.y);
      } else {
        context.lineTo(pixelPoint.x, pixelPoint.y);
      }
    });
    context.strokeStyle = "#8ff3ff";
    context.lineWidth = 2;
    context.lineCap = "round";
    context.lineJoin = "round";
    context.setLineDash([8, 6]);
    context.shadowColor = "rgba(143, 243, 255, 0.32)";
    context.shadowBlur = 12;
    context.stroke();
    context.restore();
  }

  context.textBaseline = "middle";

  activeRoundPoints.value.forEach((point, index) => {
    const pixelPoint = percentToStagePixels(point.x, point.y, rect);
    const palette = mapPointPalette[point.type];
    const selected = currentConfig.value?.selectedPointIds.includes(point.id) ?? false;
    const highlightPalette = selected ? selectedPointHighlight : palette;
    const labelFrame = getPointLabelFrame(point, rect);

    context.save();
    context.fillStyle = selected ? highlightPalette.glow : "rgba(255, 255, 255, 0.02)";
    context.beginPath();
    context.arc(pixelPoint.x, pixelPoint.y, selected ? 18 : 14, 0, Math.PI * 2);
    context.fill();

    context.fillStyle = "rgba(5, 18, 38, 0.94)";
    context.strokeStyle = highlightPalette.stroke;
    context.lineWidth = selected ? 3 : 2;
    context.beginPath();
    context.arc(pixelPoint.x, pixelPoint.y, 11, 0, Math.PI * 2);
    context.fill();
    context.shadowColor = highlightPalette.glow;
    context.shadowBlur = selected ? 18 : 12;
    context.stroke();

    drawRoundRect(context, labelFrame.left, labelFrame.top, labelFrame.width, labelFrame.height, 8);
    context.fillStyle = selected
      ? highlightPalette.labelBackground
      : "rgba(4, 19, 39, 0.82)";
    context.strokeStyle = selected
      ? highlightPalette.labelBorder
      : "rgba(91, 208, 255, 0.18)";
    context.lineWidth = 1;
    context.shadowBlur = 0;
    context.fill();
    context.stroke();

    context.fillStyle = "#eefcff";
    context.font = '700 11px "Microsoft YaHei", sans-serif';
    context.textAlign = "center";
    context.fillText(String(index + 1), pixelPoint.x, pixelPoint.y + 0.5);

    context.font = '500 12px "Microsoft YaHei", sans-serif';
    context.textAlign = "left";
    context.fillText(point.name, labelFrame.left + 8, labelFrame.top + labelFrame.height / 2 + 0.5);
    context.restore();
  });

  if (selectionBox.active) {
    const left = Math.min(selectionBox.startX, selectionBox.endX) * unitX;
    const top = Math.min(selectionBox.startY, selectionBox.endY) * unitY;
    const width = Math.abs(selectionBox.endX - selectionBox.startX) * unitX;
    const height = Math.abs(selectionBox.endY - selectionBox.startY) * unitY;

    context.save();
    context.setLineDash([6, 4]);
    context.strokeStyle = "rgba(121, 234, 255, 0.92)";
    context.fillStyle = "rgba(92, 220, 255, 0.1)";
    context.lineWidth = 1;
    context.fillRect(left, top, width, height);
    context.strokeRect(left, top, width, height);
    context.restore();
  }
}

function scheduleMapCanvasRender() {
  if (mapRenderFrame !== null) {
    return;
  }

  mapRenderFrame = window.requestAnimationFrame(() => {
    mapRenderFrame = null;
    renderMapCanvas();
  });
}

function attachMapResizeObserver() {
  if (mapResizeObserver) {
    mapResizeObserver.disconnect();
    mapResizeObserver = null;
  }

  if (!mapStageRef.value || typeof ResizeObserver === "undefined") {
    return;
  }

  mapResizeObserver = new ResizeObserver(() => {
    scheduleMapCanvasRender();
  });
  mapResizeObserver.observe(mapStageRef.value);
}

function syncBatchForm() {
  if (selectedPoints.value.length < 2) {
    batchForm.routeSpeed = "";
    batchForm.stopDuration = "";
    batchForm.liftHeight = "";
    batchForm.gimbalPreset = "";
    return;
  }

  const fields: Array<keyof BatchFormState> = [
    "routeSpeed",
    "stopDuration",
    "liftHeight",
    "gimbalPreset",
  ];

  fields.forEach((field) => {
    const values = selectedPoints.value.map((point) => point[field]);
    batchForm[field] = values.every((value) => value === values[0])
      ? values[0]
      : "";
  });
}

function updateSelection(nextIds: string[]) {
  if (!currentConfig.value) {
    return;
  }

  const validIds = activeRoundPoints.value.map((point) => point.id);
  currentConfig.value.selectedPointIds = nextIds.filter((id) =>
    validIds.includes(id),
  );
  syncBatchForm();
}

function stopMapSyncTimer() {
  if (syncTimer !== null) {
    window.clearInterval(syncTimer);
    syncTimer = null;
  }
}

function syncMapFromDevice() {
  if (!currentConfig.value || currentConfig.value.mapSyncing) {
    return;
  }

  stopMapSyncTimer();
  currentConfig.value.mapSyncing = true;
  currentConfig.value.syncProgress = 0;

  syncTimer = window.setInterval(() => {
    if (!currentConfig.value) {
      stopMapSyncTimer();
      return;
    }

    const nextProgress = Math.min(100, currentConfig.value.syncProgress + 12);
    currentConfig.value.syncProgress = nextProgress;

    if (nextProgress >= 100) {
      currentConfig.value.mapSyncing = false;
      currentConfig.value.mapLoaded = true;
      currentConfig.value.lastSyncedAt = nowString();

      const targetMap = currentConfig.value.maps.find(
        (map) => map.id === currentConfig.value?.activeMapId,
      );

      if (targetMap) {
        targetMap.updatedAt = currentConfig.value.lastSyncedAt;
        targetMap.version = `v${(Number(targetMap.version.replace(/[^\d.]/g, "")) + 0.1).toFixed(1)}`;
        targetMap.pointCloudCount += 36;
      }

      stopMapSyncTimer();
    }
  }, 160);
}

function getRelativePosition(event: MouseEvent) {
  const rect = getMapStageRect();
  if (!rect) {
    return null;
  }

  const x = ((event.clientX - rect.left) / rect.width) * 100;
  const y = ((event.clientY - rect.top) / rect.height) * 100;

  return {
    x: Math.min(96, Math.max(4, Number(x.toFixed(2)))),
    y: Math.min(92, Math.max(8, Number(y.toFixed(2)))),
  };
}

function clearSelectionBox() {
  selectionBox.active = false;
  selectionBox.moved = false;
  selectionBox.startX = 0;
  selectionBox.startY = 0;
  selectionBox.endX = 0;
  selectionBox.endY = 0;
}

function clearDragPointState() {
  dragPointState.active = false;
  dragPointState.moved = false;
  dragPointState.pointId = "";
  dragPointState.offsetX = 0;
  dragPointState.offsetY = 0;
  unbindMapPointerTracking();
}

function clampMapPosition(x: number, y: number) {
  return {
    x: Math.min(96, Math.max(4, Number(x.toFixed(2)))),
    y: Math.min(92, Math.max(8, Number(y.toFixed(2)))),
  };
}

function bindMapPointerTracking() {
  if (mapPointerTrackingBound) {
    return;
  }

  window.addEventListener("mousemove", handleWindowMapPointerMove);
  window.addEventListener("mouseup", handleWindowMapPointerUp);
  mapPointerTrackingBound = true;
}

function unbindMapPointerTracking() {
  if (!mapPointerTrackingBound) {
    return;
  }

  window.removeEventListener("mousemove", handleWindowMapPointerMove);
  window.removeEventListener("mouseup", handleWindowMapPointerUp);
  mapPointerTrackingBound = false;
}

function handleWindowMapPointerMove(event: MouseEvent) {
  handleMapPointerMove(event);
}

function handleWindowMapPointerUp(event: MouseEvent) {
  handleMapPointerUp(event);
}

function handleMapPointerDown(event: MouseEvent) {
  if (!currentConfig.value?.mapLoaded) {
    return;
  }

  const position = getRelativePosition(event);
  if (!position) {
    return;
  }

  const hitPoint = findHitPoint(position);
  if (hitPoint) {
    if (!currentConfig.value.selectedPointIds.includes(hitPoint.id)) {
      updateSelection([hitPoint.id]);
    }

    dragPointState.active = true;
    dragPointState.moved = false;
    dragPointState.pointId = hitPoint.id;
    dragPointState.offsetX = hitPoint.x - position.x;
    dragPointState.offsetY = hitPoint.y - position.y;
    bindMapPointerTracking();
    return;
  }

  if (mapEditorMode.value !== "select") {
    return;
  }

  selectionBox.active = true;
  selectionBox.moved = false;
  selectionBox.startX = position.x;
  selectionBox.startY = position.y;
  selectionBox.endX = position.x;
  selectionBox.endY = position.y;
  bindMapPointerTracking();
}

function handleMapPointerMove(event: MouseEvent) {
  if (dragPointState.active && currentConfig.value) {
    const position = getRelativePosition(event);
    if (!position) {
      return;
    }

    const targetPoint = currentConfig.value.points.find(
      (point) => point.id === dragPointState.pointId,
    );

    if (!targetPoint) {
      clearDragPointState();
      return;
    }

    const nextPosition = clampMapPosition(
      position.x + dragPointState.offsetX,
      position.y + dragPointState.offsetY,
    );

    if (
      Math.abs(nextPosition.x - targetPoint.x) > 0.05 ||
      Math.abs(nextPosition.y - targetPoint.y) > 0.05
    ) {
      dragPointState.moved = true;
    }

    targetPoint.x = nextPosition.x;
    targetPoint.y = nextPosition.y;
    scheduleMapCanvasRender();
    return;
  }

  if (!selectionBox.active) {
    return;
  }

  const position = getRelativePosition(event);
  if (!position) {
    return;
  }

  selectionBox.endX = position.x;
  selectionBox.endY = position.y;

  if (
    Math.abs(selectionBox.endX - selectionBox.startX) > 0.8 ||
    Math.abs(selectionBox.endY - selectionBox.startY) > 0.8
  ) {
    selectionBox.moved = true;
  }
}

function handleMapPointerUp(event: MouseEvent) {
  if (dragPointState.active) {
    if (dragPointState.moved) {
      suppressStageClick.value = true;
    }

    clearDragPointState();
    return;
  }

  if (!selectionBox.active || !currentConfig.value) {
    return;
  }

  if (selectionBox.moved) {
    const left = Math.min(selectionBox.startX, selectionBox.endX);
    const right = Math.max(selectionBox.startX, selectionBox.endX);
    const top = Math.min(selectionBox.startY, selectionBox.endY);
    const bottom = Math.max(selectionBox.startY, selectionBox.endY);

    const hits = activeRoundPoints.value
      .filter(
        (point) =>
          point.x >= left &&
          point.x <= right &&
          point.y >= top &&
          point.y <= bottom,
      )
      .map((point) => point.id);

    const merged = event.shiftKey
      ? Array.from(new Set([...currentConfig.value.selectedPointIds, ...hits]))
      : hits;

    updateSelection(merged);
    suppressStageClick.value = true;
  }

  clearSelectionBox();
  unbindMapPointerTracking();
}

function buildPointTypeFromMode(mode: EditorMode): PatrolPointType {
  return mode === "waypoint" ? "途经点" : "业务点";
}

function buildPointName(mode: EditorMode, type: PatrolPointType) {
  if (mode === "waypoint") {
    return `途经点 ${String(activeRoundPoints.value.length + 1).padStart(2, "0")}`;
  }

  const prefixMap: Record<PatrolPointType, string> = {
    业务点: "业务点",
    途经点: "途经点",
  };

  return `${prefixMap[type]} ${String(activeRoundPoints.value.length + 1).padStart(2, "0")}`;
}

function buildNextPointId() {
  if (!currentConfig.value) {
    return `${currentConfiguredDevice.value?.id ?? "ROBOT"}-P01`;
  }

  const maxIndex = currentConfig.value.points.reduce((maxValue, point) => {
    const match = point.id.match(/P(\d+)$/);
    return Math.max(maxValue, match ? Number(match[1]) : 0);
  }, 0);

  return `${currentConfiguredDevice.value?.id ?? "ROBOT"}-P${String(maxIndex + 1).padStart(2, "0")}`;
}

function buildNextPlanId() {
  if (!currentConfig.value) {
    return `${currentConfiguredDevice.value?.id ?? "ROBOT"}-TASK01`;
  }

  const maxIndex = currentConfig.value.plans.reduce((maxValue, plan) => {
    const match = plan.id.match(/TASK(\d+)$/);
    return Math.max(maxValue, match ? Number(match[1]) : 0);
  }, 0);

  return `${currentConfiguredDevice.value?.id ?? "ROBOT"}-TASK${String(maxIndex + 1).padStart(2, "0")}`;
}

function handleMapStageClick(event: MouseEvent) {
  if (suppressStageClick.value) {
    suppressStageClick.value = false;
    return;
  }

  if (!currentConfig.value?.mapLoaded) {
    return;
  }

  const position = getRelativePosition(event);
  if (!position) {
    return;
  }

  const hitPoint = findHitPoint(position);
  if (hitPoint) {
    handlePointClick(hitPoint.id, event);
    return;
  }

  if (mapEditorMode.value === "select") {
    updateSelection([]);
    return;
  }

  const type = buildPointTypeFromMode(mapEditorMode.value);
  const point: PatrolPoint = {
    id: buildNextPointId(),
    planId: currentConfig.value.activePlanId,
    name: buildPointName(mapEditorMode.value, type),
    type,
    direction: type === "业务点" ? "正向" : "正向",
    x: position.x,
    y: position.y,
    routeSpeed: "0.30",
    stopDuration: "10",
    liftHeight: "1500",
    gimbalPreset: "预置点1",
    note: "新建点位，待补充巡检说明",
  };

  currentConfig.value.points.push(point);
  updateSelection([point.id]);
}

function handlePointClick(pointId: string, event: MouseEvent) {
  if (!currentConfig.value) {
    return;
  }

  if (event.shiftKey || event.ctrlKey || event.metaKey) {
    const selected = currentConfig.value.selectedPointIds.includes(pointId)
      ? currentConfig.value.selectedPointIds.filter((id) => id !== pointId)
      : [...currentConfig.value.selectedPointIds, pointId];

    updateSelection(selected);
    return;
  }

  updateSelection([pointId]);
}

function clearPointSelection() {
  updateSelection([]);
}

function removeSelectedPoints() {
  if (!currentConfig.value || !currentConfig.value.selectedPointIds.length) {
    return;
  }

  currentConfig.value.points = currentConfig.value.points.filter(
    (point) => !currentConfig.value?.selectedPointIds.includes(point.id),
  );
  updateSelection([]);
}

function applyBatchConfig() {
  if (!currentConfig.value || selectedPoints.value.length < 2) {
    return;
  }

  currentConfig.value.points = currentConfig.value.points.map((point) => {
    if (!currentConfig.value?.selectedPointIds.includes(point.id)) {
      return point;
    }

    return {
      ...point,
      routeSpeed: batchForm.routeSpeed || point.routeSpeed,
      stopDuration: batchForm.stopDuration || point.stopDuration,
      liftHeight: batchForm.liftHeight || point.liftHeight,
      gimbalPreset: batchForm.gimbalPreset || point.gimbalPreset,
    };
  });

  syncBatchForm();
}

function addRound() {
  if (!currentConfig.value || !currentConfiguredDevice.value) {
    return;
  }

  const nextIndex = currentConfig.value.plans.length + 1;
  const payload: PatrolTask = {
    id: buildNextPlanId(),
    name: `第 ${nextIndex} 轮次`,
    mode: "定时",
    time: "09:00",
    cycle: "每日",
    routeRange: "全线路",
    enabled: true,
    color: nextIndex % 2 === 0 ? "#f7bf5a" : "#5ce1e6",
    loopCount: 1,
    returnMode: "结束后待命",
    speed: "0.30",
    stay: "10",
    rule: "新增轮次规则待配置。",
    updatedAt: nowString(),
  };

  currentConfig.value.plans.push(payload);
  setActiveRound(payload.id);
}

function removeRound(planId: string) {
  if (!currentConfig.value || currentConfig.value.plans.length <= 1) {
    return;
  }

  currentConfig.value.plans = currentConfig.value.plans.filter(
    (plan) => plan.id !== planId,
  );

  if (currentConfig.value.activePlanId === planId) {
    currentConfig.value.activePlanId = currentConfig.value.plans[0]?.id ?? "";
    closePlanDialog();
  }
}

function addPointToActiveRound() {
  if (!currentConfig.value?.mapLoaded) {
    return;
  }

  mapEditorMode.value = "normal";
}

function clearCanvas() {
  if (!currentConfig.value) {
    return;
  }

  currentConfig.value.points = [];
  updateSelection([]);
}

function saveConfigDialog() {
  if (!currentConfig.value || saveProgressVisible.value) {
    return;
  }

  stopSaveProgressTimers();
  currentConfig.value.lastSyncedAt = nowString();
  saveProgressVisible.value = true;
  saveProgressValue.value = 0;
  saveProgressMessage.value = "数据正在保存中...";

  saveProgressTimer = window.setInterval(() => {
    const step = saveProgressValue.value < 72 ? 12 : saveProgressValue.value < 90 ? 6 : 3;
    saveProgressValue.value = Math.min(100, saveProgressValue.value + step);

    if (saveProgressValue.value >= 100) {
      finishSaveProgress();
    }
  }, 180);
}

function editPoint(pointId: string) {
  updateSelection([pointId]);
}

function removePoint(pointId: string) {
  if (!currentConfig.value) {
    return;
  }

  currentConfig.value.points = currentConfig.value.points.filter(
    (point) => point.id !== pointId,
  );
  const nextPoint = activeRoundPoints.value.find((point) => point.id !== pointId);
  updateSelection(nextPoint ? [nextPoint.id] : []);
}

function loadPlan(point: PatrolPoint) {
  if (!activeRound.value) {
    return;
  }

  editingPointId.value = point.id;
  planForm.roundName = activeRound.value.name;
  planForm.pointName = point.name;
  planForm.pointType = point.type;
  planForm.pointDirection = point.direction;
  planForm.liftHeight = point.liftHeight;
  planForm.gimbalPreset = point.gimbalPreset;
}

function openPlanDialog(point?: PatrolPoint | null) {
  if (!point) {
    return;
  }

  loadPlan(point);
  planDialogVisible.value = true;
}

function closePlanDialog() {
  planDialogVisible.value = false;
  resetPlanForm();
}

function savePlan() {
  if (
    !currentConfig.value ||
    !activeRound.value ||
    !editingPointId.value ||
    !planForm.roundName.trim() ||
    !planForm.pointName.trim()
  ) {
    return;
  }

  currentConfig.value.plans = currentConfig.value.plans.map((plan) =>
    plan.id === currentConfig.value?.activePlanId
      ? {
          ...plan,
          name: planForm.roundName.trim(),
          updatedAt: nowString(),
        }
      : plan,
  );

  currentConfig.value.points = currentConfig.value.points.map((point) =>
    point.id === editingPointId.value
      ? {
          ...point,
          name: planForm.pointName.trim(),
          type: planForm.pointType,
          direction: planForm.pointType === "业务点" ? planForm.pointDirection : "正向",
          liftHeight:
            planForm.pointType === "业务点"
              ? planForm.liftHeight.trim() || point.liftHeight
              : "",
          gimbalPreset:
            planForm.pointType === "业务点"
              ? planForm.gimbalPreset.trim() || point.gimbalPreset
              : "",
        }
      : point,
  );

  planDialogVisible.value = false;
  resetPlanForm();
}

function removePlan(planId: string) {
  if (!currentConfig.value) {
    return;
  }

  currentConfig.value.plans = currentConfig.value.plans.filter(
    (plan) => plan.id !== planId,
  );

  if (currentConfig.value.activePlanId === planId) {
    closePlanDialog();
  }
}

onBeforeUnmount(() => {
  stopMapSyncTimer();
  unbindMapPointerTracking();
  stopSaveProgressTimers();

  if (mapRenderFrame !== null) {
    window.cancelAnimationFrame(mapRenderFrame);
    mapRenderFrame = null;
  }

  if (mapResizeObserver) {
    mapResizeObserver.disconnect();
    mapResizeObserver = null;
  }
});

watch(
  mapStageRef,
  () => {
    nextTick(() => {
      attachMapResizeObserver();
      scheduleMapCanvasRender();
    });
  },
  { flush: "post" },
);

watch(
  activeRoundPoints,
  () => {
    nextTick(() => {
      scheduleMapCanvasRender();
    });
  },
  { deep: true },
);

watch(
  () => currentConfig.value?.selectedPointIds,
  () => {
    nextTick(() => {
      scheduleMapCanvasRender();
    });
  },
  { deep: true },
);

watch(
  () => currentConfig.value?.mapLoaded,
  () => {
    nextTick(() => {
      scheduleMapCanvasRender();
    });
  },
);

watch(
  selectionBox,
  () => {
    nextTick(() => {
      scheduleMapCanvasRender();
    });
  },
  { deep: true },
);
</script>

<template>
  <div class="page-shell management-page">
    <div class="page-grid management-layout">
      <section class="page-card management-table-card">
        <div class="management-toolbar">
          <div class="toolbar-left">
            <button
              class="page-action-btn primary"
              type="button"
              @click="openCreateDialog"
            >
              新增设备
            </button>
            <input
              v-model="keyword"
              class="toolbar-input"
              placeholder="搜索设备编号 / 名称 / 云台序列号"
              type="text"
              @input="handleFilterChange"
            />
          </div>
          <div class="toolbar-right">
            共 {{ filteredDevices.length }} 台设备
          </div>
        </div>

        <div class="table-shell">
          <table class="device-table">
            <thead>
              <tr>
                <th>设备编号</th>
                <th>设备名称</th>
                <th>当前电量</th>
                <th>当前状态</th>
                <th>云台序列号</th>
                <th>创建时间</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="device in pagedDevices" :key="device.id">
                <td>{{ device.id }}</td>
                <td>{{ device.name }}</td>
                <td>{{ device.battery }}</td>
                <td>
                  <span
                    :class="['table-status', getStatusClass(device.status)]"
                  >
                    {{ device.status }}
                  </span>
                </td>
                <td>{{ device.gimbalSerial }}</td>
                <td>{{ device.createdAt }}</td>
                <td>
                  <div class="table-actions">
                    <button
                      class="config-action"
                      type="button"
                      @click="openConfigDialog(device)"
                    >
                      配置
                    </button>
                    <button type="button" @click="openEditDialog(device)">
                      编辑
                    </button>
                    <button
                      class="danger"
                      type="button"
                      @click="removeDevice(device.id)"
                    >
                      删除
                    </button>
                  </div>
                </td>
              </tr>
              <tr v-if="!pagedDevices.length">
                <td class="table-empty" colspan="7">
                  当前筛选条件下暂无设备数据
                </td>
              </tr>
            </tbody>
          </table>
        </div>

        <div class="pagination-bar">
          <button
            class="pagination-btn"
            :disabled="currentPage === 1"
            type="button"
            @click="setPage(currentPage - 1)"
          >
            上一页
          </button>
          <button
            v-for="page in visiblePages"
            :key="page"
            :class="['pagination-btn', { active: currentPage === page }]"
            type="button"
            @click="setPage(page)"
          >
            {{ page }}
          </button>
          <button
            class="pagination-btn"
            :disabled="currentPage === totalPages"
            type="button"
            @click="setPage(currentPage + 1)"
          >
            下一页
          </button>
        </div>
      </section>
    </div>

    <div
      v-if="dialogVisible"
      class="crud-dialog-mask"
      @click.self="closeDialog"
    >
      <div class="crud-dialog">
        <div class="crud-dialog-header">
          <div class="crud-dialog-title">
            {{ editingId ? "编辑设备" : "新增设备" }}
          </div>
          <button class="dialog-close" type="button" @click="closeDialog">
            ×
          </button>
        </div>
        <div class="crud-form-grid">
          <label class="crud-field">
            <span>设备名称</span>
            <input v-model="form.name" type="text" />
          </label>
          <label class="crud-field">
            <span>云台序列号</span>
            <input v-model="form.gimbalSerial" type="text" />
          </label>
          <label class="crud-field">
            <span>白光链接</span>
            <input v-model="form.whiteLightUrl" placeholder="请输入白光视频链接" type="text" />
          </label>
          <label class="crud-field">
            <span>红外链接</span>
            <input v-model="form.infraredUrl" placeholder="请输入红外视频链接" type="text" />
          </label>
        </div>
        <div v-if="formError" class="crud-form-error">{{ formError }}</div>
        <div class="crud-dialog-footer">
          <button
            class="page-action-btn ghost"
            type="button"
            @click="closeDialog"
          >
            取消
          </button>
          <button
            class="page-action-btn primary"
            type="button"
            @click="saveDevice"
          >
            保存
          </button>
        </div>
      </div>
    </div>

    <div
      v-if="configDialogVisible && currentConfiguredDevice && currentConfig"
      class="config-dialog-mask"
      @click.self="closeConfigDialog"
    >
      <div class="config-dialog">
        <div class="config-dialog-header">
          <div class="config-header-main modern">
            <div class="config-title-row">
              <h3 class="config-dialog-title">机器人配置</h3>
              <span class="config-device-chip">
                {{ currentConfiguredDevice.name }} / {{ currentConfiguredDevice.id }}
              </span>
            </div>
          </div>
          <div class="config-top-actions">
            <button class="page-action-btn ghost" type="button" @click="closeConfigDialog">
              取消
            </button>
            <button
              class="page-action-btn primary"
              :disabled="saveProgressVisible"
              type="button"
              @click="saveConfigDialog"
            >
              {{ saveProgressVisible ? "保存中..." : "保存下发" }}
            </button>
          </div>
        </div>

        <div class="config-dialog-body redesigned">
          <section class="config-map-panel redesigned-map-panel">
            <div class="panel-head modern">
              <div>
                <div class="panel-title">地图绘制主模块</div>
              </div>
              <div class="panel-actions">
                <button
                  class="page-action-btn primary"
                  type="button"
                  @click="syncMapFromDevice"
                >
                  {{
                    currentConfig.mapSyncing
                      ? "地图同步中..."
                      : "从本地设备拉取地图"
                  }}
                </button>
              </div>
            </div>

            <div class="editor-toolbar">
              <button
                v-for="mode in editorModes"
                :key="mode.id"
                :class="[
                  'editor-tool-btn',
                  { active: mapEditorMode === mode.id },
                ]"
                type="button"
                @click="mapEditorMode = mode.id"
              >
                {{ mode.label }}
              </button>
              <button class="editor-tool-btn" type="button" @click="clearCanvas">
                清空画布
              </button>
            </div>

            <div class="config-map-stage" ref="mapStageRef">
              <canvas ref="mapCanvasRef" class="config-map-canvas"></canvas>

              <div
                class="map-hit-layer"
                @click="handleMapStageClick"
                @mousedown="handleMapPointerDown"
                @mousemove="handleMapPointerMove"
                @mouseup="handleMapPointerUp"
                @mouseleave="handleMapPointerUp"
              ></div>

              <div class="stage-overlay top-left">
                {{ activeMap?.name }} / {{ activeMap?.version }}
              </div>
              <div class="stage-overlay top-right">
                {{
                  currentConfig.mapLoaded
                    ? "地图已加载，可编辑"
                    : "等待同步地图"
                }}
              </div>
            </div>
          </section>

          <section class="config-side-panel redesigned-side-panel">
            <div class="config-side-card rounds-board">
              <div class="panel-head inline">
                <div>
                  <div class="panel-title">巡航线多轮次配置</div>
                  <div class="panel-subtitle">
                    所有轮次共用同一条巡检线路，轮次表示多次往返执行
                  </div>
                </div>
              </div>

              <div class="rounds-split-layout">
                <div class="rounds-nav-column">
                  <div class="sub-panel-header">
                    <div class="sub-panel-title">轮次列表</div>
                    <button
                      class="round-add-btn"
                      type="button"
                      title="新增轮次"
                      aria-label="新增轮次"
                      @click="addRound"
                    >
                      <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M11 5h2v6h6v2h-6v6h-2v-6H5v-2h6z" />
                      </svg>
                    </button>
                  </div>
                  <div class="round-list-shell">
                    <div
                      v-for="(plan, index) in currentConfig.plans"
                      :key="plan.id"
                      :class="['round-list-item compact', { active: currentConfig.activePlanId === plan.id }]"
                      @click="setActiveRound(plan.id)"
                    >
                      <div class="round-list-main">
                        <div class="round-list-title">轮次 {{ index + 1 }}</div>
                      </div>
                      <button
                        class="round-delete-btn icon"
                        type="button"
                        title="删除轮次"
                        aria-label="删除轮次"
                        @click.stop="removeRound(plan.id)"
                      >
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                          <path
                            d="M9 3h6l1 2h4v2H4V5h4l1-2zm1 7h2v8h-2v-8zm4 0h2v8h-2v-8zM7 10h2v8H7v-8z"
                          />
                        </svg>
                      </button>
                    </div>
                  </div>
                </div>

                <div class="rounds-point-column">
                  <div class="sub-panel-title">巡检点列表</div>
                  <div class="point-list-shell">
                    <div
                      v-for="(point, index) in activeRoundPoints"
                      :key="point.id"
                      :class="[
                        'point-list-item',
                        {
                          active: currentConfig.selectedPointIds.includes(point.id),
                        },
                      ]"
                      @click="editPoint(point.id)"
                    >
                      <div class="point-list-main">
                        <div class="point-list-title">点位 {{ index + 1 }} · {{ point.name }}</div>
                        <div class="point-list-meta">
                          <span>{{ point.type }}</span>
                          <span v-if="point.type === '业务点'">{{ point.direction }}</span>
                          <span v-if="point.type === '业务点'">升降柱 {{ point.liftHeight }}</span>
                          <span v-if="point.type === '业务点'">{{ point.gimbalPreset }}</span>
                        </div>
                      </div>
                      <div class="point-list-actions">
                        <button type="button" @click.stop="openPlanDialog(point)">编辑</button>
                        <button class="danger" type="button" @click.stop="removePoint(point.id)">
                          删除
                        </button>
                      </div>
                    </div>
                    <div v-if="!activeRoundPoints.length" class="config-empty point-list-empty">
                      当前巡检线路暂无巡检点，请在左侧地图中新增点位。
                    </div>
                  </div>
                </div>
              </div>
            </div>

          </section>
        </div>

        <div v-if="planDialogVisible" class="plan-dialog-mask" @click.self="closePlanDialog">
          <div class="plan-dialog-card">
            <div class="panel-head inline">
              <div>
                <div class="panel-title">当前航线参数编辑</div>
                <div class="panel-subtitle">
                  配置当前选中轮次的执行参数
                </div>
              </div>
              <button class="dialog-close" type="button" @click="closePlanDialog">×</button>
            </div>

            <div class="config-form-grid compact plan-dialog-grid">
              <label class="config-field wide">
                <span>轮次名称</span>
                <input v-model="planForm.roundName" type="text" />
              </label>
              <label class="config-field">
                <span>巡检点名称</span>
                <input v-model="planForm.pointName" type="text" />
              </label>
              <label class="config-field">
                <span>巡检点类型</span>
                <select v-model="planForm.pointType">
                  <option v-for="type in pointTypeOptions" :key="type" :value="type">
                    {{ type }}
                  </option>
                </select>
              </label>
              <label v-if="planForm.pointType === '业务点'" class="config-field">
                <span>方向</span>
                <select v-model="planForm.pointDirection">
                  <option
                    v-for="direction in pointDirectionOptions"
                    :key="direction"
                    :value="direction"
                  >
                    {{ direction }}
                  </option>
                </select>
              </label>
              <label v-if="planForm.pointType === '业务点'" class="config-field">
                <span>升降柱高度</span>
                <input v-model="planForm.liftHeight" type="text" />
              </label>
              <label v-if="planForm.pointType === '业务点'" class="config-field">
                <span>预置点</span>
                <input v-model="planForm.gimbalPreset" type="text" />
              </label>
            </div>

            <div class="plan-editor-actions">
              <button class="page-action-btn ghost" type="button" @click="closePlanDialog">
                取消
              </button>
              <button class="page-action-btn primary" type="button" @click="savePlan">
                保存参数
              </button>
            </div>
          </div>
        </div>

        <div v-if="saveProgressVisible" class="save-progress-mask">
          <div class="save-progress-card">
            <div class="save-progress-title">保存下发</div>
            <div class="save-progress-text">{{ saveProgressMessage }}</div>
            <div class="save-progress-track">
              <div class="save-progress-fill" :style="{ width: `${saveProgressValue}%` }"></div>
            </div>
            <div class="save-progress-value">{{ saveProgressValue }}%</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped lang="less">
.config-action {
  color: #8cf7ff;
  border-color: rgba(124, 230, 255, 0.36) !important;
  background: linear-gradient(
    90deg,
    rgba(18, 82, 142, 0.9),
    rgba(20, 132, 186, 0.74)
  ) !important;
}

.config-dialog-mask {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 26px;
  background: rgba(2, 10, 22, 0.82);
  backdrop-filter: blur(8px);
}

.config-dialog {
  position: relative;
  width: min(1760px, calc(100vw - 52px));
  height: min(980px, calc(100vh - 52px));
  display: flex;
  flex-direction: column;
  padding: 22px 24px 24px;
  background:
    radial-gradient(
      circle at top left,
      rgba(20, 112, 185, 0.18),
      transparent 28%
    ),
    linear-gradient(
      180deg,
      rgba(8, 31, 62, 0.98) 0%,
      rgba(4, 15, 34, 0.98) 100%
    );
  border: 1px solid rgba(89, 208, 255, 0.24);
  box-shadow:
    0 28px 72px rgba(0, 0, 0, 0.46),
    inset 0 0 0 1px rgba(83, 193, 255, 0.08);
  overflow: hidden;
}

.config-dialog-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
}

.config-header-main {
  min-width: 0;
}

.config-header-main.modern {
  flex: 1;
}

.config-title-row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.config-dialog-title {
  margin: 0;
  color: #f2fbff;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: 0.6px;
}

.config-device-chip {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 12px;
  border: 1px solid rgba(109, 224, 255, 0.24);
  background: rgba(8, 42, 78, 0.62);
  color: #9af6ff;
  font-size: 12px;
  font-weight: 700;
}

.config-header-meta {
  margin-top: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px 22px;
  color: rgba(179, 227, 247, 0.76);
  font-size: 12px;
}

.config-top-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 10px;
}

.config-dialog-body {
  margin-top: 18px;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(360px, 0.92fr);
  gap: 16px;
}

.config-dialog-body.redesigned {
  grid-template-columns: 60% 40%;
}

.config-map-panel,
.config-side-card {
  background: linear-gradient(
    180deg,
    rgba(7, 28, 56, 0.82),
    rgba(4, 16, 34, 0.88)
  );
  border: 1px solid rgba(82, 197, 255, 0.14);
  box-shadow: inset 0 0 18px rgba(54, 170, 245, 0.04);
}

.config-map-panel {
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 18px;
}

.redesigned-map-panel {
  gap: 14px;
}

.config-side-panel {
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow: auto;
  padding-right: 2px;
}

.redesigned-side-panel {
  gap: 16px;
}

.panel-head.modern {
  align-items: center;
}

.side-tabs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.side-tab-btn {
  height: 40px;
  border: 1px solid rgba(87, 198, 255, 0.18);
  background: rgba(8, 31, 62, 0.72);
  color: rgba(192, 235, 250, 0.78);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition:
    border-color 0.2s ease,
    background 0.2s ease,
    box-shadow 0.2s ease;
}

.side-tab-btn.active {
  color: #eefcff;
  border-color: rgba(102, 235, 255, 0.7);
  background: linear-gradient(
    90deg,
    rgba(24, 102, 180, 0.88),
    rgba(35, 158, 215, 0.88)
  );
  box-shadow: 0 0 14px rgba(52, 177, 255, 0.18);
}

.config-side-card {
  padding: 16px;
}

.panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}

.panel-head.inline {
  align-items: center;
}

.panel-title {
  color: #f0fbff;
  font-size: 18px;
  font-weight: 700;
}

.panel-subtitle {
  margin-top: 6px;
  color: rgba(170, 224, 246, 0.72);
  font-size: 12px;
  line-height: 1.5;
}

.sub-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.panel-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 10px;
}

.editor-toolbar {
  margin-top: 14px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.editor-tool-btn {
  height: 34px;
  padding: 0 14px;
  border: 1px solid rgba(87, 198, 255, 0.18);
  background: rgba(8, 31, 62, 0.72);
  color: rgba(192, 235, 250, 0.78);
  font-size: 12px;
  cursor: pointer;
}

.editor-tool-btn.active {
  color: #eefcff;
  border-color: rgba(102, 235, 255, 0.7);
  background: linear-gradient(
    90deg,
    rgba(24, 102, 180, 0.88),
    rgba(35, 158, 215, 0.88)
  );
}

.editor-tip {
  margin-left: auto;
  color: rgba(161, 218, 243, 0.72);
  font-size: 12px;
}

.map-meta-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}

.meta-chip {
  padding: 12px 14px;
  border: 1px solid rgba(87, 198, 255, 0.14);
  background: rgba(8, 31, 62, 0.58);
}

.meta-chip span {
  display: block;
  color: rgba(170, 224, 246, 0.72);
  font-size: 12px;
}

.meta-chip strong {
  display: block;
  margin-top: 8px;
  color: #f2fbff;
  font-size: 14px;
}

.config-map-stage {
  position: relative;
  flex: 1;
  min-height: 420px;
  overflow: hidden;
  border: 1px solid rgba(87, 198, 255, 0.18);
  background:
    radial-gradient(
      circle at 18% 20%,
      rgba(77, 176, 229, 0.18),
      transparent 28%
    ),
    radial-gradient(
      circle at 82% 78%,
      rgba(42, 115, 178, 0.18),
      transparent 26%
    ),
    linear-gradient(135deg, rgba(5, 18, 38, 0.98), rgba(6, 28, 56, 0.96));
}

.config-map-canvas,
.map-hit-layer {
  position: absolute;
  inset: 0;
}

.map-hit-layer {
  z-index: 2;
}

.config-map-canvas {
  width: 100%;
  height: 100%;
  display: block;
}

.stage-overlay {
  position: absolute;
  z-index: 5;
  padding: 5px 10px;
  border: 1px solid rgba(91, 208, 255, 0.18);
  background: rgba(5, 20, 38, 0.72);
  color: rgba(212, 246, 255, 0.88);
  font-size: 11px;
}

.stage-overlay.top-left {
  top: 14px;
  left: 14px;
}

.stage-overlay.top-right {
  top: 14px;
  right: 14px;
}

.stage-overlay.bottom-left {
  left: 14px;
  bottom: 14px;
}

.stage-overlay.bottom-right {
  right: 14px;
  bottom: 14px;
}

.map-bottom-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  gap: 14px;
  min-height: 0;
}

.point-list-panel,
.point-editor-card {
  min-height: 0;
  padding: 16px;
  background: linear-gradient(
    180deg,
    rgba(7, 28, 56, 0.82),
    rgba(4, 16, 34, 0.88)
  );
  border: 1px solid rgba(82, 197, 255, 0.14);
  box-shadow: inset 0 0 18px rgba(54, 170, 245, 0.04);
}

.sub-panel-title {
  color: #f0fbff;
  font-size: 16px;
  font-weight: 700;
}

.round-add-btn {
  width: 28px;
  min-width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(87, 198, 255, 0.2);
  background: rgba(8, 31, 62, 0.72);
  color: #8cf7ff;
  cursor: pointer;
}

.round-add-btn svg {
  width: 14px;
  height: 14px;
  fill: currentColor;
}

.point-tag-row {
  margin-top: 12px;
  display: flex;
  gap: 8px;
}

.point-type-tag,
.point-id-tag {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 10px;
  border: 1px solid rgba(91, 208, 255, 0.18);
  font-size: 12px;
}

.point-type-tag.type-normal {
  background: rgba(39, 149, 207, 0.18);
}

.point-type-tag.type-thermal {
  background: rgba(185, 108, 37, 0.18);
}

.point-type-tag.type-pump {
  background: rgba(38, 156, 112, 0.18);
}

.point-id-tag {
  color: rgba(170, 224, 246, 0.76);
}

.config-form-grid {
  margin-top: 14px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.config-form-grid.compact {
  margin-top: 12px;
}

.config-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  color: rgba(173, 224, 246, 0.78);
  font-size: 12px;
}

.config-field.wide {
  grid-column: 1 / -1;
}

.config-field input,
.config-field select,
.config-field textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid rgba(85, 198, 255, 0.18);
  background: rgba(8, 31, 62, 0.72);
  color: #eefcff;
  font-size: 13px;
  box-sizing: border-box;
  outline: none;
}

.config-field textarea {
  resize: vertical;
  min-height: 88px;
}

.config-empty {
  margin-top: 12px;
  padding: 16px;
  border: 1px dashed rgba(87, 198, 255, 0.16);
  background: rgba(7, 24, 47, 0.58);
  color: rgba(164, 218, 243, 0.72);
  font-size: 12px;
  line-height: 1.7;
}

.batch-tip {
  margin-top: 12px;
  color: rgba(165, 221, 245, 0.76);
  font-size: 12px;
}

.batch-apply-btn {
  margin-top: 12px;
  width: 100%;
}

.switch-field {
  justify-content: flex-end;
}

.plan-switch {
  height: 38px;
  border: 1px solid rgba(87, 198, 255, 0.18);
  background: rgba(8, 31, 62, 0.72);
  color: rgba(192, 235, 250, 0.78);
  cursor: pointer;
}

.plan-switch.active {
  color: #eefcff;
  border-color: rgba(102, 235, 255, 0.7);
  background: linear-gradient(
    90deg,
    rgba(24, 102, 180, 0.88),
    rgba(35, 158, 215, 0.88)
  );
}

.plan-editor-actions {
  margin-top: 12px;
  display: flex;
  gap: 10px;
}

.plan-dialog-mask {
  position: absolute;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background: rgba(2, 10, 22, 0.62);
  backdrop-filter: blur(6px);
}

.plan-dialog-card {
  width: min(760px, calc(100% - 32px));
  padding: 18px;
  background: linear-gradient(
    180deg,
    rgba(8, 31, 62, 0.98) 0%,
    rgba(4, 15, 34, 0.98) 100%
  );
  border: 1px solid rgba(89, 208, 255, 0.24);
  box-shadow:
    0 24px 56px rgba(0, 0, 0, 0.42),
    inset 0 0 0 1px rgba(83, 193, 255, 0.08);
}

.plan-dialog-grid {
  margin-top: 16px;
}

.save-progress-mask {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background: rgba(2, 10, 22, 0.62);
  backdrop-filter: blur(6px);
}

.save-progress-card {
  width: min(420px, calc(100% - 32px));
  padding: 22px 24px;
  background: linear-gradient(
    180deg,
    rgba(8, 31, 62, 0.98) 0%,
    rgba(4, 15, 34, 0.98) 100%
  );
  border: 1px solid rgba(89, 208, 255, 0.24);
  box-shadow:
    0 24px 56px rgba(0, 0, 0, 0.42),
    inset 0 0 0 1px rgba(83, 193, 255, 0.08);
}

.save-progress-title {
  color: #f2fbff;
  font-size: 18px;
  font-weight: 700;
}

.save-progress-text {
  margin-top: 10px;
  color: rgba(179, 227, 247, 0.82);
  font-size: 13px;
}

.save-progress-track {
  margin-top: 18px;
  height: 10px;
  overflow: hidden;
  border: 1px solid rgba(87, 198, 255, 0.16);
  background: rgba(8, 31, 62, 0.62);
}

.save-progress-fill {
  height: 100%;
  background: linear-gradient(
    90deg,
    rgba(24, 102, 180, 0.92) 0%,
    rgba(35, 158, 215, 0.9) 100%
  );
  box-shadow: 0 0 18px rgba(52, 177, 255, 0.26);
  transition: width 0.18s ease;
}

.save-progress-value {
  margin-top: 12px;
  color: #9af6ff;
  font-size: 13px;
  font-weight: 700;
  text-align: right;
}

.plan-list-shell {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 300px;
  overflow: auto;
}

.plan-list-item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid rgba(84, 197, 255, 0.12);
  background: rgba(8, 31, 62, 0.62);
}

.plan-item-main {
  min-width: 0;
}

.plan-item-title {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #f2fbff;
  font-size: 14px;
  font-weight: 700;
}

.plan-status-chip {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 8px;
  border: 1px solid rgba(255, 148, 105, 0.2);
  background: rgba(126, 31, 20, 0.22);
  color: #ffd0cc;
  font-size: 11px;
}

.plan-status-chip.active {
  border-color: rgba(110, 255, 197, 0.2);
  background: rgba(29, 138, 102, 0.18);
  color: #9cf7dc;
}

.plan-item-meta,
.plan-item-updated {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  color: rgba(171, 223, 246, 0.76);
  font-size: 12px;
}

.plan-item-actions {
  display: flex;
  gap: 8px;
}

.plan-item-actions button {
  height: 30px;
  padding: 0 10px;
  border: 1px solid rgba(87, 198, 255, 0.2);
  background: rgba(8, 31, 62, 0.72);
  color: #eafcff;
  font-size: 12px;
  cursor: pointer;
}

.plan-item-actions .danger {
  border-color: rgba(255, 126, 112, 0.2);
  color: #ffd0cc;
  background: rgba(126, 31, 20, 0.22);
}

.rounds-board {
  padding: 16px;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.rounds-split-layout {
  margin-top: 14px;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(180px, 36%) minmax(0, 64%);
  gap: 14px;
}

.rounds-nav-column,
.rounds-point-column {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 12px;
  border: 1px solid rgba(87, 198, 255, 0.14);
  box-shadow: inset 0 0 18px rgba(54, 170, 245, 0.04);
}

.rounds-nav-column {
  background: linear-gradient(
    180deg,
    rgba(6, 24, 46, 0.82),
    rgba(5, 18, 36, 0.9)
  );
}

.rounds-point-column {
  background: linear-gradient(
    180deg,
    rgba(8, 34, 62, 0.84),
    rgba(6, 22, 42, 0.92)
  );
}

.round-list-shell {
  margin-top: 14px;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: auto;
}

.round-list-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  border: 1px solid rgba(84, 197, 255, 0.12);
  background: rgba(8, 31, 62, 0.62);
  cursor: pointer;
}

.round-list-item.compact {
  gap: 8px;
  align-items: center;
  padding: 10px 10px 10px 12px;
}

.round-list-item.active {
  border-color: rgba(102, 235, 255, 0.42);
  background: rgba(10, 48, 88, 0.88);
}

.round-list-main,
.point-list-main {
  min-width: 0;
  flex: 1;
}

.round-list-item.compact .round-list-title {
  font-size: 13px;
  line-height: 1.2;
}

.round-list-item.compact .round-list-meta {
  margin-top: 4px;
  font-size: 11px;
}

.round-list-title,
.point-list-title {
  color: #f2fbff;
  font-size: 14px;
  font-weight: 700;
}

.round-list-meta,
.point-list-meta {
  margin-top: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 10px;
  color: rgba(171, 223, 246, 0.76);
  font-size: 12px;
}

.round-delete-btn,
.point-list-actions button {
  height: 30px;
  padding: 0 10px;
  border: 1px solid rgba(87, 198, 255, 0.2);
  background: rgba(8, 31, 62, 0.72);
  color: #eafcff;
  font-size: 12px;
  cursor: pointer;
}

.round-delete-btn.icon {
  width: 28px;
  min-width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.round-delete-btn.icon svg {
  width: 14px;
  height: 14px;
  fill: currentColor;
}

.point-list-shell {
  margin-top: 14px;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: auto;
}

.point-list-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid rgba(84, 197, 255, 0.12);
  background: rgba(8, 31, 62, 0.62);
  cursor: pointer;
}

.point-list-empty {
  margin-top: 0;
}

.point-list-item.active {
  border-color: rgba(255, 196, 118, 0.52);
  background: rgba(96, 56, 18, 0.62);
  box-shadow: inset 0 0 0 1px rgba(255, 196, 118, 0.12);
}

.point-list-actions {
  display: flex;
  gap: 8px;
}

.point-list-actions .danger,
.round-delete-btn,
.page-action-btn.danger {
  border-color: rgba(255, 126, 112, 0.2);
  color: #ffd0cc;
  background: rgba(126, 31, 20, 0.22);
}

.point-editor-grid {
  margin-top: 14px;
}

.round-summary-strip {
  margin-top: 14px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

.summary-item {
  padding: 12px;
  border: 1px solid rgba(87, 198, 255, 0.14);
  background: rgba(8, 31, 62, 0.54);
}

.summary-item span {
  display: block;
  color: rgba(170, 224, 246, 0.72);
  font-size: 12px;
}

.summary-item strong {
  display: block;
  margin-top: 6px;
  color: #f2fbff;
  font-size: 14px;
}

.page-action-btn.amber {
  color: #ffe0a8;
  border-color: rgba(245, 190, 88, 0.34) !important;
  background: linear-gradient(
    90deg,
    rgba(118, 78, 16, 0.88),
    rgba(168, 116, 32, 0.74)
  ) !important;
}

@media (max-width: 1480px) {
  .config-dialog-body {
    grid-template-columns: 1fr;
  }

  .config-side-panel {
    overflow: visible;
  }

  .config-dialog {
    height: auto;
    max-height: calc(100vh - 52px);
    overflow: auto;
  }

  .config-dialog-body.redesigned,
  .map-meta-strip,
  .map-bottom-grid,
  .round-summary-strip {
    grid-template-columns: 1fr 1fr;
  }

  .rounds-split-layout {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 960px) {
  .config-top-actions,
  .config-dialog-header {
    flex-direction: column;
  }

  .config-top-actions {
    width: 100%;
  }

  .config-top-actions .page-action-btn {
    width: 100%;
  }

  .map-meta-strip,
  .map-bottom-grid,
  .round-summary-strip {
    grid-template-columns: 1fr;
  }
}
</style>
