<script lang="ts" setup>
import { NImage } from "naive-ui";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import {
  getRecentAlarmList,
  type RosInspectionRecordLineImage,
  type RosInspectionRecordLineImageQueryDTO,
} from "@/api/alarm";
import {
  dispatchRosInspectionTask,
  getCurrentRosInspection,
  getRosCarList,
  getRosInspectionTaskDetail,
  stopDispatchRosInspectionTask,
} from "@/api/rosCar";
import { getToken } from "@/utils/auth";
import { parseRosMapYaml } from "@/utils/rosMapYaml";
import { BASE_URL } from "@/utils/request";
import { getRecentAlertPhoto } from "../shared/alertPhotos";

const defaultProject = "西江隧道";

type DriveMode = "阿克曼" | "旋转" | "横移";
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
type ControlMessageType = "cmd_vel" | "ptz_control" | "lift_control";
type PtzAction = "up" | "down" | "left" | "right" | "stop";

interface RosCarRecord {
  id?: string;
  deviceKey?: string;
  deviceNo?: string;
  name?: string;
  batteryLevel?: number;
  status?: number;
  projectName?: string;
  poseX?: number;
  poseY?: number;
  poseYaw?: number;
  mapPgmUrl?: string;
  mapYamlUrl?: string;
  cameraWebrtcUrl?: string;
  cameraWebrtcUrl2?: string;
  liftHeight?: number;
  liftDeviceStatusDescription?: string;
}

interface RosCarListResult {
  data?: RosCarRecord[];
}

interface RosCarListResponse {
  data?: RosCarListResult;
}

interface RecentAlarmListResponse {
  data?: RosInspectionRecordLineImage[];
}

interface CarStatusPushData {
  deviceKey?: string;
  liftHeight?: number;
  liftDeviceStatusDescription?: string;
  batteryLevel?: number;
  voltage?: number;
  poseX?: number;
  poseY?: number;
  poseYaw?: number;
  mapPgmUrl?: string;
  mapYamlUrl?: string;
  status?: number;
  signal?: string;
}

interface CarVelocityPushData {
  deviceKey?: string;
  velocity?: {
    linearX?: number;
    angularZ?: number;
  };
}

type ControlSocketMessageType =
  | "control_result"
  | "car_status"
  | "car_velocity"
  | "inspection_update"
  | "inspection_record_stopped"
  | "warning_update";

interface ControlSocketMessage {
  type?: ControlSocketMessageType | string;
  data?: {
    accepted?: boolean;
    message?: string;
  } & CarStatusPushData & CarVelocityPushData;
}

interface DeviceItem {
  id: string;
  carId: string;
  deviceKey: string;
  name: string;
  status: DeviceStatus;
  location: string;
  battery: string;
  task: string;
  linearSpeed: string;
  angularSpeed: string;
  liftHeight: string;
  liftStatus: string;
  poseX?: number;
  poseY?: number;
  poseYaw?: number;
  mapPgmUrl: string;
  mapYamlUrl: string;
  whiteLightUrl: string;
  infraredUrl: string;
}

interface ProjectItem {
  id: number;
  name: string;
  devices: DeviceItem[];
}

interface NavPoint {
  x: number;
  y: number;
}

interface NavScenario {
  routePath: string;
  lanePath: string;
  upperEdgePath: string;
  lowerEdgePath: string;
  upperWallPoints: NavPoint[];
  lowerWallPoints: NavPoint[];
  checkPoints: Array<{ x: number; y: number; label: string }>;
}

interface RosMapMeta {
  resolution: number;
  origin: [number, number, number];
}

interface RosMapState extends RosMapMeta {
  imageUrl: string;
  width: number;
  height: number;
  viewBox: string;
}

interface RobotMapPoint {
  x: number;
  y: number;
  yaw: number;
  angle: number;
}

interface InspectionRouteNode {
  type?: "via_point" | "action_point";
  poseX: number;
  poseY: number;
  nodeIndex: number;
}

interface InspectionTaskDetail {
  id?: string;
  nodes?: InspectionRouteNode[];
}

interface InspectionTaskDetailResponse {
  data?: InspectionTaskDetail;
}

interface RosInspectionRecord {
  taskId?: string;
  roundCount?: number;
  currentRoundIndex?: number;
  startTime?: string;
  status?: number;
  currentNodeIndex?: number;
  totalActionCount?: number;
}

interface CurrentInspectionResponse {
  data?: RosInspectionRecord | null;
}

interface RosDispatchResultResponse {
  data?: {
    recordId?: string;
    message?: string;
  };
  msg?: string;
}

interface AlertItem {
  id: string;
  title: string;
  result: string;
  time: string;
  photo: string;
}

const emptyDevice: DeviceItem = {
  id: "--",
  carId: "",
  deviceKey: "",
  name: "暂无设备",
  status: "离线",
  location: "位置未上报",
  battery: "--",
  task: "暂无任务",
  linearSpeed: "--",
  angularSpeed: "--",
  liftHeight: "--",
  liftStatus: "--",
  poseX: undefined,
  poseY: undefined,
  poseYaw: undefined,
  mapPgmUrl: "",
  mapYamlUrl: "",
  whiteLightUrl: "",
  infraredUrl: "",
};
const projectDevices = ref<ProjectItem[]>([
  {
    id: 1,
    name: defaultProject,
    devices: [],
  },
]);

// 将接口的小车状态码转换为集成中心展示状态
function normalizeDeviceStatus(status?: number): DeviceStatus {
  const statusMap: Record<number, DeviceStatus> = {
    0: "空闲",
    1: "巡检",
    2: "离线",
    3: "充电中",
  };

  return statusMap[status ?? 2] ?? "离线";
}

// 根据状态生成当前任务说明
function buildDeviceTask(status: DeviceStatus) {
  const taskMap: Record<DeviceStatus, string> = {
    空闲: "待命",
    巡检: "巡检中",
    离线: "离线",
    充电中: "回库充电",
  };

  return taskMap[status];
}

// 将设备管理列表接口数据转换为集成中心设备卡片数据
function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceItem {
  const status = normalizeDeviceStatus(car.status);
  const location = typeof car.poseX === "number" && typeof car.poseY === "number"
    ? `X:${car.poseX.toFixed(2)} / Y:${car.poseY.toFixed(2)}`
    : "位置未上报";

  const carId = car.id || "";
  const deviceKey = car.deviceKey || car.deviceNo || car.id || `ROBOT-${index + 1}`;

  return {
    id: car.deviceNo || car.id || deviceKey,
    carId,
    deviceKey,
    name: car.name || "--",
    status,
    location,
    battery: typeof car.batteryLevel === "number" ? `${car.batteryLevel}%` : "--",
    task: buildDeviceTask(status),
    linearSpeed: "--",
    angularSpeed: "--",
    liftHeight: typeof car.liftHeight === "number" ? String(car.liftHeight) : "--",
    liftStatus: car.liftDeviceStatusDescription || "--",
    poseX: car.poseX,
    poseY: car.poseY,
    poseYaw: car.poseYaw,
    mapPgmUrl: car.mapPgmUrl || "",
    mapYamlUrl: car.mapYamlUrl || "",
    whiteLightUrl: car.cameraWebrtcUrl || "",
    infraredUrl: car.cameraWebrtcUrl2 || "",
  };
}

// 复用设备管理页的小车分页列表数据刷新集成中心设备列表
async function fetchDeviceList() {
  try {
    const response = (await getRosCarList({
      page: 1,
      limit: 100,
      query: {},
    })) as RosCarListResponse;
    const rows = response.data?.data ?? [];
    const projectName = rows[0]?.projectName || defaultProject;

    const devices = rows.map((car, index) => normalizeDeviceRecord(car, index));

    projectDevices.value = [
      {
        id: 1,
        name: projectName,
        devices,
      },
    ];

    if (!devices.some((device) => device.id === activeDeviceId.value)) {
      activeDeviceId.value = devices[0]?.id ?? "";
      syncLiftTargetFromDevice(devices[0]);
    }
  } catch (error) {
    console.error(error);
  }
}

// 请求近期告警接口并刷新监管中心告警卡片
async function fetchRecentAlerts() {
  alertLoading.value = true;
  alertError.value = "";

  const query: RosInspectionRecordLineImageQueryDTO = {
    recognizeType: recognizeTypeCodeMap[activeAlertTab.value],
  };

  try {
    const response = (await getRecentAlarmList(query)) as RecentAlarmListResponse;
    const rows = Array.isArray(response.data) ? response.data : [];

    recentAlerts.value = rows.map((record, index) => normalizeAlertRecord(record, index));
  } catch (error) {
    alertError.value = "近期告警加载失败";
    recentAlerts.value = [];
    console.error(error);
  } finally {
    alertLoading.value = false;
  }
}

// 将后端识别记录转换为监管中心告警卡片数据
function normalizeAlertRecord(record: RosInspectionRecordLineImage, index: number): AlertItem {
  const title = getRecognizeTypeText(record.recognizeType);

  return {
    id: record.id || record.recordActionId || record.recordId || `AL-${index + 1}`,
    title,
    result: record.warningValue || record.recognitionResult || record.failReason || "--",
    time: record.captureTime || record.createTime || "--",
    photo: buildFileAccessUrl(record.imageUrl || "") || getRecentAlertPhoto(title),
  };
}

function getRecognizeTypeText(type?: number) {
  return type ? recognizeTypeMap[type] ?? "未知告警" : "未知告警";
}

// 相对文件地址通过接口代理访问，避免开发环境直接请求后端静态路径失败
function buildFileAccessUrl(fileUrl: string) {
  if (
    !fileUrl ||
    /^(https?:)?\/\//i.test(fileUrl) ||
    fileUrl.startsWith("blob:") ||
    fileUrl.startsWith("data:")
  ) {
    return fileUrl;
  }

  if (fileUrl === BASE_URL || fileUrl.startsWith(`${BASE_URL}/`)) {
    return fileUrl;
  }

  return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
}

function readPgmToken(bytes: Uint8Array, cursor: { index: number }) {
  while (cursor.index < bytes.length) {
    const code = bytes[cursor.index];

    if (code === 35) {
      while (cursor.index < bytes.length && bytes[cursor.index] !== 10) {
        cursor.index += 1;
      }
    } else if (code <= 32) {
      cursor.index += 1;
    } else {
      break;
    }
  }

  const start = cursor.index;
  while (cursor.index < bytes.length && bytes[cursor.index] > 32) {
    cursor.index += 1;
  }

  return new TextDecoder("ascii").decode(bytes.slice(start, cursor.index));
}

// 将 ROS PGM 黑白占据栅格重绘为大屏风格底图
function buildStyledPgmMap(buffer: ArrayBuffer) {
  const bytes = new Uint8Array(buffer);
  const cursor = { index: 0 };
  const magic = readPgmToken(bytes, cursor);
  const width = Number(readPgmToken(bytes, cursor));
  const height = Number(readPgmToken(bytes, cursor));
  const maxValue = Number(readPgmToken(bytes, cursor));

  if (magic !== "P5" || !width || !height || !maxValue) {
    throw new Error("暂只支持 P5 二进制 PGM 地图");
  }

  while (cursor.index < bytes.length && bytes[cursor.index] <= 32) {
    cursor.index += 1;
  }

  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");

  if (!context) {
    throw new Error("浏览器不支持 Canvas 地图渲染");
  }

  canvas.width = width;
  canvas.height = height;
  const imageData = context.createImageData(width, height);
  const twoBytePixel = maxValue > 255;

  for (let index = 0; index < width * height; index += 1) {
    const sourceIndex = cursor.index + (twoBytePixel ? index * 2 : index);
    const rawValue = twoBytePixel
      ? ((bytes[sourceIndex] ?? 0) << 8) + (bytes[sourceIndex + 1] ?? 0)
      : bytes[sourceIndex] ?? 0;
    const gray = Math.round((rawValue / maxValue) * 255);
    const targetIndex = index * 4;

    if (gray < 80) {
      imageData.data[targetIndex] = 70;
      imageData.data[targetIndex + 1] = 226;
      imageData.data[targetIndex + 2] = 255;
      imageData.data[targetIndex + 3] = 220;
    } else if (gray > 220) {
      imageData.data[targetIndex] = 16;
      imageData.data[targetIndex + 1] = 78;
      imageData.data[targetIndex + 2] = 124;
      imageData.data[targetIndex + 3] = 128;
    } else {
      imageData.data[targetIndex] = 9;
      imageData.data[targetIndex + 1] = 22;
      imageData.data[targetIndex + 2] = 42;
      imageData.data[targetIndex + 3] = 42;
    }
  }

  context.putImageData(imageData, 0, 0);
  return {
    imageUrl: canvas.toDataURL("image/png"),
    width,
    height,
  };
}

const recognizeTypeMap: Record<number, string> = {
  100: "局部高温",
  1: "电缆表面破损",
  2: "桥架断裂",
  3: "隧道积水",
  4: "墙体裂缝",
};
const recognizeTypeCodeMap = Object.fromEntries(
  Object.entries(recognizeTypeMap).map(([value, label]) => [label, Number(value)]),
) as Record<string, number>;

const recentAlerts = ref<AlertItem[]>([]);
const mapScale = ref(1);
const mapOffset = ref({ x: 0, y: 0 });
const mapDragState = ref({
  dragging: false,
  startX: 0,
  startY: 0,
  originX: 0,
  originY: 0,
});
const rosMap = ref<RosMapState | null>(null);
const mapLoading = ref(false);
const mapError = ref("");
const routeNodes = ref<InspectionRouteNode[]>([]);
const routeLoading = ref(false);
const routeError = ref("");
const inspectionTaskId = ref("");
const currentInspection = ref<RosInspectionRecord | null>(null);
const inspectionLoading = ref(false);
const inspectionActionLoading = ref(false);
const lastDispatchTime = ref("");
const alertLoading = ref(false);
const alertError = ref("");
const alertTabs = ["局部高温", "电缆表面破损", "桥架断裂", "隧道积水", "墙体裂缝"] as const;
const activeAlertTab = ref<(typeof alertTabs)[number]>("局部高温");
const activeCameraMode = ref<"white" | "infrared">("white");
const activeDeviceId = ref("");
const driveModeTabs: DriveMode[] = ["阿克曼", "旋转", "横移"];
const activeDriveMode = ref<DriveMode>("阿克曼");
const presetPointOptions = Array.from(
  { length: 10 },
  (_, index) => `预置点${index + 1}`,
);
const selectedPresetPoint = ref("预置点1");
const controlMode = ref<"人工接管" | "自主巡检">("自主巡检");
const maxLiftHeight = 800;
const targetLiftHeight = ref("1200");
const monitorVideoRef = ref<HTMLVideoElement | null>(null);
const navMapSvgRef = ref<SVGSVGElement | null>(null);
const streamLoading = ref(false);
const streamError = ref("");
const controlWsStatus = ref("未连接");
const controlError = ref("");
let streamPeerConnection: RTCPeerConnection | null = null;
let streamRequestId = 0;
let controlSocket: WebSocket | null = null;
let controlRequestSeed = 0;
let controlReconnectTimer: number | null = null;
let controlSocketManualClose = false;
let chassisControlTimer: number | null = null;
let ptzControlTimer: number | null = null;
// let inspectionPollTimer: number | null = null;
let chassisControlling = false;
let ptzControlling = false;

const navScenario: NavScenario = {
  routePath:
    "M 106 318 L 154 306 L 208 292 L 256 278 L 306 264 L 358 258 L 414 258 L 468 248 L 522 241 L 578 234 L 638 226",
  lanePath:
    "M 82 270 L 136 258 L 190 244 L 244 230 L 296 216 L 350 208 L 408 208 L 464 198 L 518 190 L 576 182 L 642 174 L 656 188 L 650 278 L 586 286 L 528 294 L 472 302 L 414 312 L 358 312 L 304 320 L 252 334 L 198 348 L 142 360 L 88 370 L 74 356 Z",
  upperEdgePath:
    "M 82 270 L 136 258 L 190 244 L 244 230 L 296 216 L 350 208 L 408 208 L 464 198 L 518 190 L 576 182 L 642 174",
  lowerEdgePath:
    "M 88 370 L 142 360 L 198 348 L 252 334 L 304 320 L 358 312 L 414 312 L 472 302 L 528 294 L 586 286 L 650 278",
  upperWallPoints: [
    { x: 64, y: 164 },
    { x: 84, y: 150 },
    { x: 104, y: 142 },
    { x: 126, y: 136 },
    { x: 148, y: 132 },
    { x: 172, y: 126 },
    { x: 196, y: 122 },
    { x: 224, y: 118 },
    { x: 252, y: 116 },
    { x: 280, y: 112 },
    { x: 312, y: 110 },
    { x: 344, y: 108 },
    { x: 378, y: 108 },
    { x: 412, y: 110 },
    { x: 448, y: 112 },
    { x: 484, y: 114 },
    { x: 520, y: 116 },
    { x: 556, y: 120 },
    { x: 590, y: 126 },
    { x: 622, y: 134 },
    { x: 650, y: 144 },
    { x: 676, y: 158 },
    { x: 116, y: 170 },
    { x: 214, y: 156 },
    { x: 326, y: 146 },
    { x: 446, y: 148 },
    { x: 566, y: 160 },
    { x: 638, y: 172 },
  ],
  lowerWallPoints: [
    { x: 72, y: 380 },
    { x: 96, y: 392 },
    { x: 122, y: 404 },
    { x: 150, y: 410 },
    { x: 180, y: 416 },
    { x: 214, y: 420 },
    { x: 248, y: 422 },
    { x: 284, y: 424 },
    { x: 322, y: 426 },
    { x: 360, y: 426 },
    { x: 400, y: 424 },
    { x: 440, y: 420 },
    { x: 480, y: 414 },
    { x: 520, y: 408 },
    { x: 558, y: 400 },
    { x: 594, y: 392 },
    { x: 628, y: 384 },
    { x: 660, y: 374 },
    { x: 700, y: 366 },
    { x: 126, y: 376 },
    { x: 238, y: 392 },
    { x: 354, y: 396 },
    { x: 474, y: 388 },
    { x: 590, y: 372 },
  ],
  checkPoints: [
    { x: 106, y: 318, label: "起点" },
    { x: 358, y: 258, label: "途经点" },
    { x: 638, y: 226, label: "终点" },
  ],
};

const currentProject = computed(() => projectDevices.value[0]);
const currentDevices = computed(() => currentProject.value?.devices ?? []);
const currentRobot = computed(
  () => currentDevices.value.find((device) => device.id === activeDeviceId.value) ?? currentDevices.value[0] ?? emptyDevice,
);
const currentMonitorVideo = computed(() => activeCameraMode.value === "white"
  ? currentRobot.value.whiteLightUrl
  : currentRobot.value.infraredUrl,
);
const currentScenario = computed(() => navScenario);
const hasRobotPose = computed(
  () => typeof currentRobot.value.poseX === "number" && typeof currentRobot.value.poseY === "number",
);

// 将 ROS 米制坐标转换成地图图片像素坐标，路线和机器人定位共用同一套转换
function rosPoseToMapPoint(poseX: number, poseY: number, poseYaw = 0): RobotMapPoint | null {
  if (!rosMap.value) {
    return null;
  }

  const x = (poseX - rosMap.value.origin[0]) / rosMap.value.resolution;
  const y = rosMap.value.height - (poseY - rosMap.value.origin[1]) / rosMap.value.resolution;

  return {
    x: Math.min(Math.max(x, 0), rosMap.value.width),
    y: Math.min(Math.max(y, 0), rosMap.value.height),
    yaw: poseYaw,
    angle: -poseYaw * 180 / Math.PI,
  };
}

const robotMapPoint = computed(() => {
  if (!hasRobotPose.value) {
    return null;
  }

  return rosPoseToMapPoint(
    currentRobot.value.poseX as number,
    currentRobot.value.poseY as number,
    currentRobot.value.poseYaw ?? 0,
  );
});
const robotMarkerScale = computed(() => {
  if (!rosMap.value) {
    return 1;
  }

  return 1 / mapScale.value;
});
const mapContentTransform = computed(() => {
  if (!rosMap.value) {
    return "";
  }

  const centerX = rosMap.value.width / 2;
  const centerY = rosMap.value.height / 2;

  return `translate(${mapOffset.value.x}, ${mapOffset.value.y}) translate(${centerX}, ${centerY}) scale(${mapScale.value}) translate(${-centerX}, ${-centerY})`;
});
const routeMapPoints = computed(() => routeNodes.value
  .map((node) => rosPoseToMapPoint(node.poseX, node.poseY))
  .filter((point): point is RobotMapPoint => Boolean(point)));
const realRoutePath = computed(() => routeMapPoints.value
  .map((point, index) => `${index === 0 ? "M" : "L"} ${point.x.toFixed(2)} ${point.y.toFixed(2)}`)
  .join(" "));
const realCheckPoints = computed(() => routeMapPoints.value.map((point, index) => ({
  ...point,
  label: index === 0 ? "起点" : index === routeMapPoints.value.length - 1 ? "终点" : "途经点",
})));
const inspectionRunning = computed(() => currentInspection.value?.status === 0);
const inspectionStatusText = computed(() => inspectionRunning.value ? "巡检中" : "空闲");
const currentRoundText = computed(() => {
  if (!inspectionRunning.value) {
    return "--/--";
  }

  return `${currentInspection.value?.currentRoundIndex ?? "--"}/${currentInspection.value?.roundCount ?? "--"}`;
});
const currentInspectionPointText = computed(() => {
  if (!inspectionRunning.value) {
    return "--/--";
  }

  return `${currentInspection.value?.currentNodeIndex ?? "--"}/${currentInspection.value?.totalActionCount ?? "--"}`;
});

const filteredRecentAlerts = computed(() =>
  recentAlerts.value.filter((alert) => alert.title === activeAlertTab.value),
);

// 加载当前机器人携带的 PGM/YAML 地图，并转换为可叠加真实坐标的前端地图
async function loadRobotMap(device: DeviceItem) {
  const mapPgmUrl = device.mapPgmUrl;
  const mapYamlUrl = device.mapYamlUrl;

  rosMap.value = null;
  mapError.value = "";

  if (!mapPgmUrl || !mapYamlUrl) {
    mapError.value = "当前机器人未配置地图文件";
    return;
  }

  mapLoading.value = true;

  try {
    const [pgmResponse, yamlResponse] = await Promise.all([
      fetch(buildFileAccessUrl(mapPgmUrl)),
      fetch(buildFileAccessUrl(mapYamlUrl)),
    ]);

    if (!pgmResponse.ok) {
      throw new Error("地图 PGM 文件访问失败");
    }

    if (!yamlResponse.ok) {
      throw new Error("地图 YAML 文件访问失败");
    }

    const styledMap = buildStyledPgmMap(await pgmResponse.arrayBuffer());
    const parsedMeta = parseRosMapYaml(await yamlResponse.text());
    const meta: RosMapMeta = {
      resolution: parsedMeta.resolution ?? 0.05,
      origin: parsedMeta.origin ?? [0, 0, 0],
    };

    rosMap.value = {
      ...meta,
      ...styledMap,
      viewBox: `0 0 ${styledMap.width} ${styledMap.height}`,
    };
  } catch (error) {
    console.error(error);
    mapError.value = error instanceof Error ? error.message : "机器人地图加载失败";
  } finally {
    mapLoading.value = false;
  }
}

// 调用巡检任务详情接口，将路线节点渲染到真实 ROS 地图上，并记录下发所需 taskId
async function loadRobotRoute(device: DeviceItem) {
  routeNodes.value = [];
  routeError.value = "";
  inspectionTaskId.value = "";

  const cardId = device.carId || device.id;

  if (!cardId || cardId === "--") {
    return;
  }

  routeLoading.value = true;

  try {
    const response = (await getRosInspectionTaskDetail(cardId)) as InspectionTaskDetailResponse;
    inspectionTaskId.value = response.data?.id || "";
    routeNodes.value = [...(response.data?.nodes ?? [])]
      .filter((node) => typeof node.poseX === "number" && typeof node.poseY === "number")
      .sort((current, next) => current.nodeIndex - next.nodeIndex);
  } catch (error) {
    console.error(error);
    routeError.value = "巡检路线加载失败";
  } finally {
    routeLoading.value = false;
  }
}

// 刷新当前巡检状态，非执行中统一按空闲展示
async function fetchCurrentInspection(silent = false) {
  const carId = currentRobot.value.carId || currentRobot.value.id;

  if (!carId || carId === "--") {
    currentInspection.value = null;
    return;
  }

  if (!silent) {
    inspectionLoading.value = true;
  }

  try {
    const response = (await getCurrentRosInspection(carId)) as CurrentInspectionResponse;
    const record = response.data?.status === 0 ? response.data : null;

    currentInspection.value = record;

    if (response.data?.startTime) {
      lastDispatchTime.value = response.data.startTime;
    }
  } catch (error) {
    console.error(error);
  } finally {
    inspectionLoading.value = false;
  }
}

// 巡检状态轮询暂时关闭，后续需要恢复时取消下面代码注释
// function stopInspectionPolling() {
//   if (inspectionPollTimer !== null) {
//     window.clearInterval(inspectionPollTimer);
//     inspectionPollTimer = null;
//   }
// }

// function startInspectionPolling() {
//   stopInspectionPolling();
//   fetchCurrentInspection();
//   inspectionPollTimer = window.setInterval(() => {
//     fetchCurrentInspection(true);
//   }, 10000);
// }

function getDispatchTaskId() {
  return currentInspection.value?.taskId || inspectionTaskId.value;
}

function showConfirmDialog(title: string, content: string, onConfirm: () => void | Promise<void>) {
  window.$modal.warning({
    title,
    content,
    positiveText: "确认",
    negativeText: "取消",
    maskClosable: false,
    onPositiveClick: onConfirm,
  });
}

async function runInspectionAction(action: "dispatch" | "stop") {
  const taskId = getDispatchTaskId();

  if (!taskId) {
    window.$message.warning("未获取到巡检任务ID，请先在设备管理中保存巡检任务");
    return;
  }

  inspectionActionLoading.value = true;

  try {
    const response = action === "dispatch"
      ? await dispatchRosInspectionTask(taskId) as RosDispatchResultResponse
      : await stopDispatchRosInspectionTask(taskId) as RosDispatchResultResponse;
    const message = response.data?.message || response.msg || (action === "dispatch" ? "巡检任务已下发" : "已停止巡检任务");

    window.$message.success(message);
    await fetchCurrentInspection();
  } catch (error) {
    console.error(error);
  } finally {
    inspectionActionLoading.value = false;
  }
}

function confirmDispatchInspection() {
  showConfirmDialog("下发巡检", "确认下发当前巡检任务吗？", () => runInspectionAction("dispatch"));
}

function confirmStopInspection() {
  showConfirmDialog("停止巡检", "确认停止当前巡检任务吗？", () => runInspectionAction("stop"));
}

// 切换设备时将当前设备已有升降柱高度回显到目标高度输入框
function syncLiftTargetFromDevice(device?: DeviceItem) {
  if (!device || device.liftHeight === "--") {
    return;
  }

  targetLiftHeight.value = device.liftHeight;
}

// 根据当前页面地址生成控制 WebSocket 地址，开发环境由 Vite 代理转发 /ws
function buildControlWsUrl() {
  const token = getToken();
  const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
  const wsUrl = new URL(`${wsProtocol}//${window.location.host}/ws/ros`);

  if (token) {
    wsUrl.searchParams.set("token", token);
  }

  return wsUrl.toString();
}

// 用推送中的设备标识查找当前设备列表索引
function findDeviceIndexByKey(deviceKey?: string) {
  if (!deviceKey) {
    return -1;
  }

  return currentDevices.value.findIndex((device) => device.deviceKey === deviceKey || device.id === deviceKey);
}

// 将小车状态推送合并到设备列表，实时刷新电量、位置、状态和视频地址
function applyCarStatusPush(data: CarStatusPushData) {
  const deviceIndex = findDeviceIndexByKey(data.deviceKey);

  if (deviceIndex < 0) {
    return;
  }

  const currentDevice = currentDevices.value[deviceIndex];
  const status = typeof data.status === "number"
    ? normalizeDeviceStatus(data.status)
    : currentDevice.status;
  const location = typeof data.poseX === "number" && typeof data.poseY === "number"
    ? `X:${data.poseX.toFixed(2)} / Y:${data.poseY.toFixed(2)}`
    : currentDevice.location;
  const nextDevice: DeviceItem = {
    ...currentDevice,
    deviceKey: data.deviceKey || currentDevice.deviceKey,
    status,
    location,
    battery: typeof data.batteryLevel === "number" ? `${data.batteryLevel}%` : currentDevice.battery,
    task: buildDeviceTask(status),
    liftHeight: typeof data.liftHeight === "number" ? String(data.liftHeight) : currentDevice.liftHeight,
    liftStatus: data.liftDeviceStatusDescription || currentDevice.liftStatus,
    poseX: typeof data.poseX === "number" ? data.poseX : currentDevice.poseX,
    poseY: typeof data.poseY === "number" ? data.poseY : currentDevice.poseY,
    poseYaw: typeof data.poseYaw === "number" ? data.poseYaw : currentDevice.poseYaw,
    mapPgmUrl: data.mapPgmUrl || currentDevice.mapPgmUrl,
    mapYamlUrl: data.mapYamlUrl || currentDevice.mapYamlUrl,
  };

  projectDevices.value[0].devices.splice(deviceIndex, 1, nextDevice);

  if (!activeDeviceId.value) {
    activeDeviceId.value = nextDevice.id;
  }
}

// 将小车速度推送合并到设备列表，实时刷新线速度和角速度
function applyCarVelocityPush(data: CarVelocityPushData) {
  const deviceIndex = findDeviceIndexByKey(data.deviceKey);

  if (deviceIndex < 0 || !data.velocity) {
    return;
  }

  const currentDevice = currentDevices.value[deviceIndex];
  const nextDevice: DeviceItem = {
    ...currentDevice,
    linearSpeed: typeof data.velocity.linearX === "number"
      ? `${data.velocity.linearX.toFixed(2)}m/s`
      : currentDevice.linearSpeed,
    angularSpeed: typeof data.velocity.angularZ === "number"
      ? `${data.velocity.angularZ.toFixed(2)}rad/s`
      : currentDevice.angularSpeed,
  };

  projectDevices.value[0].devices.splice(deviceIndex, 1, nextDevice);
}

// 控制 WebSocket 异常断开后延迟重连，避免刷新进入页面后保持未连接状态
function scheduleControlReconnect() {
  if (controlSocketManualClose || controlReconnectTimer !== null) {
    return;
  }

  controlWsStatus.value = "重连中";
  controlReconnectTimer = window.setTimeout(() => {
    controlReconnectTimer = null;
    connectControlSocket();
  }, 2000);
}

// 建立浏览器到 ROS 控制网关的 WebSocket 连接
function connectControlSocket() {
  if (
    controlSocket &&
    (controlSocket.readyState === WebSocket.OPEN || controlSocket.readyState === WebSocket.CONNECTING)
  ) {
    return controlSocket;
  }

  controlSocketManualClose = false;
  controlError.value = "";
  controlWsStatus.value = "连接中";
  controlSocket = new WebSocket(buildControlWsUrl());

  controlSocket.onopen = () => {
    controlWsStatus.value = "已连接";
  };

  controlSocket.onmessage = (event) => {
    if (event.data === "ping") {
      controlSocket?.send("pong");
      return;
    }

    if (event.data === "pong") {
      return;
    }

    try {
      const message = JSON.parse(event.data) as ControlSocketMessage;

      if (message.type === "control_result" && message.data?.accepted === false) {
        controlError.value = message.data.message || "控制命令未被接受";
        return;
      }

      if (message.type === "car_status" && message.data) {
        applyCarStatusPush(message.data);
        return;
      }

      if (message.type === "car_velocity" && message.data) {
        applyCarVelocityPush(message.data);
        return;
      }

      if (message.type === "inspection_update" || message.type === "inspection_record_stopped") {
        fetchCurrentInspection(true);
        return;
      }

      if (message.type === "warning_update") {
        fetchRecentAlerts();
      }
    } catch (error) {
      console.error(error);
    }
  };

  controlSocket.onerror = () => {
    controlError.value = "控制 WebSocket 连接异常";
  };

  controlSocket.onclose = () => {
    controlWsStatus.value = "未连接";
    controlSocket = null;
    scheduleControlReconnect();
  };

  return controlSocket;
}

// 关闭控制 WebSocket，页面卸载时释放连接
function closeControlSocket() {
  controlSocketManualClose = true;

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

  if (!controlSocket) {
    controlWsStatus.value = "未连接";
    return;
  }

  controlSocket.close();
  controlSocket = null;
  controlWsStatus.value = "未连接";
}

// 通过 WS 下发机器人远程控制消息
function sendControlMessage(type: ControlMessageType, data: Record<string, string | number>) {
  if (!currentRobot.value.deviceKey) {
    controlError.value = "当前机器人缺少设备标识";
    return;
  }

  const socket = connectControlSocket();
  const payload = {
    type,
    requestId: `${type}-${Date.now()}-${controlRequestSeed += 1}`,
    data: {
      deviceKey: currentRobot.value.deviceKey,
      ...data,
    },
  };
  const sendPayload = () => {
    controlError.value = "";
    socket.send(JSON.stringify(payload));
  };

  if (socket.readyState === WebSocket.OPEN) {
    sendPayload();
    return;
  }

  socket.addEventListener("open", sendPayload, { once: true });
}

// 停止当前 WebRTC 拉流并释放播放器资源
function stopMonitorStream() {
  streamRequestId += 1;
  streamLoading.value = false;

  if (streamPeerConnection) {
    streamPeerConnection.close();
    streamPeerConnection = null;
  }

  if (monitorVideoRef.value) {
    monitorVideoRef.value.srcObject = null;
  }
}

// 将设备返回的 webrtc 播放地址转换为 ZLMediaKit WebRTC 播放接口地址
function buildWebrtcApiUrl(streamUrl: string) {
  const normalizedUrl = streamUrl.replace(/^webrtc:\/\//, "https://");
  const url = new URL(normalizedUrl);

  return `${url.origin}${url.pathname}${url.search}`;
}

// 等待本地 ICE 候选收集完成，确保发给流媒体服务的 offer 信息完整
function waitForIceGatheringComplete(peerConnection: RTCPeerConnection) {
  if (peerConnection.iceGatheringState === "complete") {
    return Promise.resolve();
  }

  return new Promise<void>((resolve) => {
    const handleIceGatheringStateChange = () => {
      if (peerConnection.iceGatheringState === "complete") {
        peerConnection.removeEventListener("icegatheringstatechange", handleIceGatheringStateChange);
        resolve();
      }
    };

    peerConnection.addEventListener("icegatheringstatechange", handleIceGatheringStateChange);
  });
}

// 通过 WebRTC offer/answer 拉取当前机器人视频流
async function playMonitorStream() {
  stopMonitorStream();
  streamError.value = "";

  if (!currentMonitorVideo.value) {
    return;
  }

  if (!window.RTCPeerConnection) {
    streamError.value = "当前浏览器不支持 WebRTC 播放";
    return;
  }

  const requestId = streamRequestId;
  const peerConnection = new RTCPeerConnection();
  streamPeerConnection = peerConnection;
  streamLoading.value = true;

  peerConnection.addTransceiver("video", { direction: "recvonly" });
  peerConnection.addTransceiver("audio", { direction: "recvonly" });
  peerConnection.ontrack = (event) => {
    if (requestId !== streamRequestId || !monitorVideoRef.value) {
      return;
    }

    monitorVideoRef.value.srcObject = event.streams[0];
  };

  try {
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    await waitForIceGatheringComplete(peerConnection);

    const response = await fetch(buildWebrtcApiUrl(currentMonitorVideo.value), {
      method: "POST",
      headers: {
        "Content-Type": "application/sdp",
      },
      body: peerConnection.localDescription?.sdp ?? offer.sdp,
    });

    if (!response.ok) {
      throw new Error(`拉流失败：${response.status}`);
    }

    const responseText = await response.text();
    let answer = responseText;

    try {
      const responseData = JSON.parse(responseText) as { code?: number; sdp?: string; msg?: string };

      if (responseData.code && responseData.code !== 0) {
        throw new Error(responseData.msg || "视频流拉取失败");
      }

      answer = responseData.sdp || responseText;
    } catch (error) {
      if (responseText.trim().startsWith("{")) {
        throw error;
      }
    }

    if (requestId !== streamRequestId) {
      return;
    }

    await peerConnection.setRemoteDescription({
      type: "answer",
      sdp: answer,
    });
  } catch (error) {
    if (requestId === streamRequestId) {
      streamError.value = error instanceof Error ? error.message : "视频流拉取失败";
      stopMonitorStream();
    }
  } finally {
    if (requestId === streamRequestId) {
      streamLoading.value = false;
    }
  }
}

onMounted(() => {
  fetchDeviceList();
  fetchRecentAlerts();
  connectControlSocket();
  // startInspectionPolling();
  window.addEventListener("blur", stopContinuousControl);
});

onBeforeUnmount(() => {
  stopMonitorStream();
  stopContinuousControl();
  // stopInspectionPolling();
  closeControlSocket();
  window.removeEventListener("blur", stopContinuousControl);
});

watch(
  currentMonitorVideo,
  () => {
    nextTick(() => {
      playMonitorStream();
    });
  },
);

watch(
  [
    () => currentRobot.value.id,
    () => currentRobot.value.carId,
    () => currentRobot.value.mapPgmUrl,
    () => currentRobot.value.mapYamlUrl,
  ],
  () => {
    currentInspection.value = null;
    lastDispatchTime.value = "";
    loadRobotMap(currentRobot.value);
    loadRobotRoute(currentRobot.value);
    fetchCurrentInspection();
  },
  { immediate: true },
);

function switchAlertTab(tab: (typeof alertTabs)[number]) {
  activeAlertTab.value = tab;
  fetchRecentAlerts();
}

function switchCameraMode(mode: "white" | "infrared") {
  activeCameraMode.value = mode;
}

// 点击设备列表中的机器人后，同步切换控制对象和监控视频源
function selectDevice(device: DeviceItem) {
  activeDeviceId.value = device.id;
  syncLiftTargetFromDevice(device);
}

function switchDriveMode(mode: DriveMode) {
  activeDriveMode.value = mode;
}

// 调整真实 ROS 地图整体缩放，不影响底部状态浮层
function changeMapScale(delta: number) {
  mapScale.value = Math.min(3, Math.max(0.55, Number((mapScale.value + delta).toFixed(2))));
}

function resetMapScale() {
  mapScale.value = 1;
  mapOffset.value = { x: 0, y: 0 };
}

// 鼠标滚轮缩放地图内容，按住拖拽平移地图视野
function handleMapWheel(event: WheelEvent) {
  event.preventDefault();
  changeMapScale(event.deltaY > 0 ? -0.05 : 0.05);
}

function startMapDrag(event: PointerEvent) {
  if (!rosMap.value || event.button !== 0) {
    return;
  }

  mapDragState.value = {
    dragging: true,
    startX: event.clientX,
    startY: event.clientY,
    originX: mapOffset.value.x,
    originY: mapOffset.value.y,
  };
}

function moveMapDrag(event: PointerEvent) {
  if (!mapDragState.value.dragging || !rosMap.value || !navMapSvgRef.value) {
    return;
  }

  const rect = navMapSvgRef.value.getBoundingClientRect();
  const svgDeltaX = (event.clientX - mapDragState.value.startX) * (rosMap.value.width / rect.width);
  const svgDeltaY = (event.clientY - mapDragState.value.startY) * (rosMap.value.height / rect.height);

  mapOffset.value = {
    x: mapDragState.value.originX + svgDeltaX,
    y: mapDragState.value.originY + svgDeltaY,
  };
}

function stopMapDrag() {
  mapDragState.value.dragging = false;
}

// 单次下发底盘速度控制
function sendCmdVel(linearX: number, angularZ: number) {
  sendControlMessage("cmd_vel", { linearX, angularZ });
}

// 按住底盘按钮时高频下发速度控制，保持 ROS cmd_vel 持续刷新
function startCmdVel(linearX: number, angularZ: number) {
  if (chassisControlTimer !== null) {
    window.clearInterval(chassisControlTimer);
  }

  chassisControlling = true;
  sendCmdVel(linearX, angularZ);
  chassisControlTimer = window.setInterval(() => {
    sendCmdVel(linearX, angularZ);
  }, 50);
}

// 停止底盘运动，仅在发生过底盘控制后发送停止，避免刷新/失焦产生无效停止消息
function stopCmdVel() {
  if (chassisControlTimer !== null) {
    window.clearInterval(chassisControlTimer);
    chassisControlTimer = null;
  }

  if (!chassisControlling) {
    return;
  }

  chassisControlling = false;
  sendControlMessage("cmd_vel", { linearX: 0, angularZ: 0 });
}

// 单次下发云台方向控制
function sendPtzControl(action: PtzAction) {
  sendControlMessage("ptz_control", { action });
}

// 按住云台按钮时高频下发方向控制，保持云台连续动作
function startPtzControl(action: PtzAction) {
  if (ptzControlTimer !== null) {
    window.clearInterval(ptzControlTimer);
  }

  ptzControlling = true;
  sendPtzControl(action);
  ptzControlTimer = window.setInterval(() => {
    sendPtzControl(action);
  }, 50);
}

// 停止云台运动，仅在发生过云台控制后发送 stop
function stopPtzControl() {
  if (ptzControlTimer !== null) {
    window.clearInterval(ptzControlTimer);
    ptzControlTimer = null;
  }

  if (!ptzControlling) {
    return;
  }

  ptzControlling = false;
  sendControlMessage("ptz_control", { action: "stop" });
}

// 页面失焦时停止连续控制动作，避免机器人持续运动
function stopContinuousControl() {
  stopCmdVel();
  stopPtzControl();
}

function toggleControlMode() {
  controlMode.value =
    controlMode.value === "人工接管" ? "自主巡检" : "人工接管";
}

function jumpToPresetPoint() {
  selectedPresetPoint.value = selectedPresetPoint.value;
}

// 下发升降柱控制命令，control_type 参照 WS 控制协议
function sendLiftControl(controlType: number, height = 0) {
  sendControlMessage("lift_control", {
    control_type: controlType,
    height,
  });
}

function resetLiftHeight() {
  targetLiftHeight.value = "0";
  sendLiftControl(0);
}

function raiseLiftHeight() {
  sendLiftControl(1);
}

function lowerLiftHeight() {
  sendLiftControl(2);
}

function stopLiftHeight() {
  sendLiftControl(3);
}

function runToTargetHeight() {
  const normalizedHeight = targetLiftHeight.value.replace(/[^\d]/g, "");
  const height = Math.min(maxLiftHeight, Number(normalizedHeight) || 0);

  targetLiftHeight.value = String(height);
  sendLiftControl(4, height);
}

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

  return classMap[status];
}
</script>

<template>
  <div class="center">
    <div class="left">
      <div class="card-1 device-card">
        <div class="title">设备列表</div>
        <div class="device-panel">
          <div class="device-list">
            <template v-if="currentDevices.length">
              <div v-for="device in currentDevices" :key="device.id"
                :class="['device-item', { active: currentRobot.id === device.id }]" @click="selectDevice(device)">
                <div class="device-main">
                  <div class="device-name">{{ device.name }}</div>
                  <div :class="['device-status', getStatusClass(device.status)]">
                    {{ device.status }}
                  </div>
                </div>
                <div class="device-meta">
                  <span>{{ device.id }}</span>
                  <span>{{ device.battery }}</span>
                </div>
              </div>
            </template>
            <div v-else class="device-empty">暂无设备数据</div>
          </div>
        </div>
      </div>

      <div class="card-2 alert-card">
        <div class="title">近期告警</div>
        <div class="alert-panel">
          <div class="alert-tabs">
            <button v-for="tab in alertTabs" :key="tab" :class="['alert-tab', { active: activeAlertTab === tab }]"
              type="button" @click="switchAlertTab(tab)">
              {{ tab }}
            </button>
          </div>
          <div class="alert-list">
            <div v-if="alertLoading" class="alert-empty">近期告警加载中...</div>
            <div v-else-if="alertError" class="alert-empty">{{ alertError }}</div>
            <template v-else-if="filteredRecentAlerts.length">
              <div v-for="alert in filteredRecentAlerts" :key="alert.id" class="alert-item">
                <div class="alert-content">
                  <NImage
                    class="alert-thumbnail"
                    :src="alert.photo"
                    :alt="alert.title"
                    object-fit="cover"
                    :previewed-img-props="{ style: { maxWidth: '82vw', maxHeight: '82vh', objectFit: 'contain' } }"
                  />
                  <div class="alert-info">
                    <div class="alert-summary">
                      <div class="alert-field">
                        <div class="alert-label">告警类型</div>
                        <div class="alert-title">{{ alert.title }}</div>
                      </div>
                      <div class="alert-field alert-field-right">
                        <div class="alert-label">识别结果</div>
                        <div class="alert-result">{{ alert.result }}</div>
                      </div>
                    </div>
                    <div class="alert-time-row">
                      <span class="alert-label">告警时间</span>
                      <span class="alert-time">{{ alert.time }}</span>
                    </div>
                  </div>
                </div>
              </div>
            </template>
            <div v-else class="alert-empty">当前筛选下暂无告警</div>
          </div>
        </div>
      </div>
    </div>

    <div class="middle">
      <div class="nav-map-panel">
        <div class="nav-map-stage">
          <svg v-if="rosMap" ref="navMapSvgRef" :class="['nav-map-svg', 'real-map-svg', { dragging: mapDragState.dragging }]" :viewBox="rosMap.viewBox" preserveAspectRatio="xMidYMid meet"
            @wheel="handleMapWheel" @pointerdown="startMapDrag" @pointermove="moveMapDrag" @pointerup="stopMapDrag"
            @pointerleave="stopMapDrag" @pointercancel="stopMapDrag">
            <defs>
              <filter id="robotGlow" x="-80%" y="-80%" width="260%" height="260%">
                <feGaussianBlur stdDeviation="2.5" result="blur" />
                <feMerge>
                  <feMergeNode in="blur" />
                  <feMergeNode in="SourceGraphic" />
                </feMerge>
              </filter>
            </defs>
            <g class="real-map-content" :transform="mapContentTransform">
              <image class="ros-map-image" :href="rosMap.imageUrl" x="0" y="0" :width="rosMap.width" :height="rosMap.height" />
              <path v-if="realRoutePath" class="route-glow" :d="realRoutePath" />
              <path v-if="realRoutePath" class="route-line" :d="realRoutePath" />
              <g class="checkpoint-layer">
                <g v-for="point in realCheckPoints" :key="`${point.label}-${point.x}-${point.y}`" class="checkpoint"
                  :transform="`translate(${point.x}, ${point.y})`">
                  <circle r="3"></circle>
                  <text x="7" y="2.5">{{ point.label }}</text>
                </g>
              </g>
              <g v-if="robotMapPoint" class="robot-layer real-robot-layer"
                :transform="`translate(${robotMapPoint.x}, ${robotMapPoint.y}) rotate(${robotMapPoint.angle}) scale(${robotMarkerScale})`">
                <circle class="robot-shadow" r="6.5" />
                <circle class="robot-body" r="5.8" />
                <path class="robot-head" d="M -1.6 -5.4 L 4.8 0 L -1.6 5.4 Z" />
              </g>
            </g>
          </svg>
          <svg v-else class="nav-map-svg" viewBox="0 0 760 420" preserveAspectRatio="xMidYMid meet">
            <g class="grid-layer">
              <line v-for="line in 7" :key="`h-${line}`" :x1="40" :y1="line * 52" :x2="720" :y2="line * 52" />
              <line v-for="line in 12" :key="`v-${line}`" :x1="line * 56" :y1="40" :x2="line * 56" :y2="380" />
            </g>
            <path class="corridor-lane" :d="currentScenario.lanePath" />
            <path class="corridor-edge" :d="currentScenario.upperEdgePath" />
            <path class="corridor-edge" :d="currentScenario.lowerEdgePath" />
            <g class="wall-layer wall-upper">
              <circle v-for="(point, index) in currentScenario.upperWallPoints" :key="`upper-wall-${index}`"
                :cx="point.x" :cy="point.y" :r="index % 4 === 0 ? 3.4 : 2.4" />
            </g>
            <g class="wall-layer wall-lower">
              <circle v-for="(point, index) in currentScenario.lowerWallPoints" :key="`lower-wall-${index}`"
                :cx="point.x" :cy="point.y" :r="index % 5 === 0 ? 3.2 : 2.3" />
            </g>
            <path class="route-glow" :d="currentScenario.routePath" />
            <path class="route-line" :d="currentScenario.routePath" />
            <g class="checkpoint-layer">
              <g v-for="point in currentScenario.checkPoints" :key="point.label" class="checkpoint"
                :transform="`translate(${point.x}, ${point.y})`">
                <circle r="7"></circle>
                <text x="12" y="4">{{ point.label }}</text>
              </g>
            </g>
            <g class="robot-layer">
              <circle class="robot-shadow" r="18" cx="358" cy="258" />
              <circle class="robot-body" r="10" cx="358" cy="258" />
              <path class="robot-head" d="M 355 249 L 366 258 L 355 267 Z" />
            </g>
          </svg>
          <div v-if="mapLoading" class="map-state-tip">地图加载中...</div>
          <div v-else-if="mapError" class="map-state-tip">{{ mapError }}</div>
          <div v-else-if="routeLoading" class="map-state-tip">路线加载中...</div>
          <div v-else-if="routeError" class="map-state-tip">{{ routeError }}</div>
          <div class="nav-map-overlay top-left">ROS 实时地图</div>
          <div class="nav-map-overlay top-right">真实坐标路径跟踪</div>
          <div v-if="rosMap" class="map-scale-controls">
            <button type="button" @click="changeMapScale(-0.05)">-</button>
            <span>{{ Math.round(mapScale * 100) }}%</span>
            <button type="button" @click="changeMapScale(0.05)">+</button>
            <button type="button" @click="resetMapScale">重置</button>
          </div>
          <div class="nav-map-overlay bottom-left">
            机器人编号：{{ currentRobot.id }} / {{ currentRobot.task }}
          </div>
          <div class="nav-map-overlay bottom-right">
            当前位置 {{ currentRobot.location }} / 电量 {{ currentRobot.battery }} / 线速度
            {{ currentRobot.linearSpeed }} / 角速度 {{ currentRobot.angularSpeed }}
          </div>
        </div>
      </div>
    </div>

    <div class="right">
      <div class="card-1 video-card">
        <div class="card-header">
          <div class="title">视频监控</div>
          <div class="camera-switch header-switch">
            <button :class="['camera-btn', { active: activeCameraMode === 'white' }]" type="button"
              @click="switchCameraMode('white')">
              白光
            </button>
            <button :class="['camera-btn', { active: activeCameraMode === 'infrared' }]" type="button"
              @click="switchCameraMode('infrared')">
              红外
            </button>
          </div>
        </div>
        <div class="video-panel">
          <div class="video-screen">
            <video v-if="currentMonitorVideo" ref="monitorVideoRef" :key="currentMonitorVideo"
              :class="['monitor-video', { infrared: activeCameraMode === 'infrared' }]" autoplay muted
              playsinline></video>
            <div class="scan-line"></div>
            <!-- <div class="video-overlay top-left">实时视频流</div>
            <div class="video-overlay top-right">
              {{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
            </div> -->
            <div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
            <div v-if="streamLoading" class="video-empty">视频流连接中...</div>
            <div v-else-if="streamError" class="video-empty">{{ streamError }}</div>
            <div v-else-if="!currentMonitorVideo" class="video-empty">当前机器人暂无视频地址</div>
          </div>
        </div>
      </div>

      <div class="card-2 control-card">
        <div class="card-header">
          <div class="title">远程控制</div>
          <button class="mode-toggle-btn" type="button" @click="toggleControlMode">
            <span :class="['mode-toggle-chip', { active: controlMode === '自主巡检' }]">
              自主巡检
            </span>
            <span :class="['mode-toggle-chip', { active: controlMode === '人工接管' }]">
              人工接管
            </span>
          </button>
        </div>
        <div class="control-panel">
          <div class="control-status">
            <div class="status-item">
              <span class="status-label">控制对象</span>
              <span class="status-value">{{ currentRobot.id }}</span>
            </div>
            <div class="status-item">
              <span class="status-label">当前模式</span>
              <span class="status-value">{{ controlMode }}</span>
            </div>
          </div>
          <div v-if="controlMode === '人工接管'" class="control-main">
            <div class="control-top-row">
              <div class="vehicle-section">
                <div class="section-title control-block-title">底盘驱动</div>
                <!-- <div class="drive-mode-tabs">
                  <button
                    v-for="mode in driveModeTabs"
                    :key="mode"
                    :class="['drive-mode-tab', { active: activeDriveMode === mode }]"
                    type="button"
                    @click="switchDriveMode(mode)"
                  >
                    {{ mode }}
                  </button>
                </div> -->
                <div class="vehicle-pad">
                  <button class="drive-btn drive-up" type="button" @pointerdown.prevent="startCmdVel(0.25, 0)"
                    @pointerup="stopCmdVel" @pointerleave="stopCmdVel" @pointercancel="stopCmdVel">
                    <span class="drive-arrow">↑</span>
                    <span class="drive-label">前进</span>
                  </button>
                  <button class="drive-btn drive-left" type="button" @pointerdown.prevent="startCmdVel(0.25, 0.25)"
                    @pointerup="stopCmdVel" @pointerleave="stopCmdVel" @pointercancel="stopCmdVel">
                    <span class="drive-arrow">←</span>
                    <span class="drive-label">左转</span>
                  </button>
                  <div class="drive-core" aria-hidden="true"></div>
                  <button class="drive-btn drive-right" type="button" @pointerdown.prevent="startCmdVel(0.25, -0.25)"
                    @pointerup="stopCmdVel" @pointerleave="stopCmdVel" @pointercancel="stopCmdVel">
                    <span class="drive-arrow">→</span>
                    <span class="drive-label">右转</span>
                  </button>
                  <button class="drive-btn drive-down" type="button" @pointerdown.prevent="startCmdVel(-0.25, 0)"
                    @pointerup="stopCmdVel" @pointerleave="stopCmdVel" @pointercancel="stopCmdVel">
                    <span class="drive-arrow">↓</span>
                    <span class="drive-label">后退</span>
                  </button>
                </div>
              </div>
              <div class="control-section gimbal-section">
                <div class="section-title">云台控制</div>
                <!-- <div class="gimbal-preset-panel">
                  <label class="gimbal-preset-group">
                    <select v-model="selectedPresetPoint" class="gimbal-preset-select">
                      <option
                        v-for="preset in presetPointOptions"
                        :key="preset"
                        :value="preset"
                      >
                        {{ preset }}
                      </option>
                    </select>
                  </label>
                  <button class="gimbal-preset-btn" type="button" @click="jumpToPresetPoint">
                    跳转
                  </button>
                </div> -->
                <div class="vehicle-pad gimbal-wheel">
                  <button class="drive-btn drive-up" type="button" @pointerdown.prevent="startPtzControl('up')"
                    @pointerup="stopPtzControl" @pointerleave="stopPtzControl"
                    @pointercancel="stopPtzControl">
                    <span class="drive-arrow">↑</span>
                    <span class="drive-label">上</span>
                  </button>
                  <button class="drive-btn drive-left" type="button" @pointerdown.prevent="startPtzControl('left')"
                    @pointerup="stopPtzControl" @pointerleave="stopPtzControl"
                    @pointercancel="stopPtzControl">
                    <span class="drive-arrow">←</span>
                    <span class="drive-label">左</span>
                  </button>
                  <div class="drive-core" aria-hidden="true"></div>
                  <button class="drive-btn drive-right" type="button" @pointerdown.prevent="startPtzControl('right')"
                    @pointerup="stopPtzControl" @pointerleave="stopPtzControl"
                    @pointercancel="stopPtzControl">
                    <span class="drive-arrow">→</span>
                    <span class="drive-label">右</span>
                  </button>
                  <button class="drive-btn drive-down" type="button" @pointerdown.prevent="startPtzControl('down')"
                    @pointerup="stopPtzControl" @pointerleave="stopPtzControl"
                    @pointercancel="stopPtzControl">
                    <span class="drive-arrow">↓</span>
                    <span class="drive-label">下</span>
                  </button>
                </div>
              </div>
            </div>
            <div class="control-section lift-section">
              <div class="section-title">升降柱控制</div>
              <div class="lift-control-panel">
                <label class="lift-input-group">
                  <span class="lift-input-label">目标高度</span>
                  <div class="lift-input-shell">
                    <input
                      v-model="targetLiftHeight"
                      class="lift-input"
                      type="text"
                      inputmode="numeric"
                      maxlength="4"
                      :placeholder="`0-${maxLiftHeight}`"
                    />
                    <span class="lift-unit">mm</span>
                  </div>
                </label>
                <div class="lift-action-row">
                  <button class="lift-submit-btn" type="button" @click="resetLiftHeight">
                    复位
                  </button>
                  <button class="lift-submit-btn" type="button" @click="raiseLiftHeight">
                    上升
                  </button>
                  <button class="lift-submit-btn" type="button" @click="lowerLiftHeight">
                    下降
                  </button>
                  <button class="lift-submit-btn" type="button" @click="stopLiftHeight">
                    停止
                  </button>
                  <button class="lift-submit-btn lift-submit-wide" type="button" @click="runToTargetHeight">
                    运行到指定高度
                  </button>
                </div>
                <div class="lift-status-panel">
                  <div class="lift-status-item">
                    <span class="lift-status-label">实时状态</span>
                    <span class="lift-status-value">{{ currentRobot.liftStatus }}</span>
                  </div>
                  <div class="lift-status-item">
                    <span class="lift-status-label">当前高度</span>
                    <span class="lift-status-value">{{ currentRobot.liftHeight }} mm</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div v-else class="inspection-panel">
            <div class="inspection-state-card">
              <div class="inspection-state-head">
                <span class="inspection-state-label">巡检状态</span>
                <span :class="['inspection-state-value', { running: inspectionRunning }]">
                  {{ inspectionStatusText }}
                </span>
              </div>
              <div class="inspection-state-sub">
                {{ inspectionLoading ? "状态刷新中..." : "收到巡检事件后自动刷新" }}
              </div>
            </div>
            <div class="inspection-info-grid">
              <div class="inspection-info-item">
                <span class="inspection-info-label">上次下发时间</span>
                <span class="inspection-info-value">{{ lastDispatchTime || "--" }}</span>
              </div>
              <template v-if="inspectionRunning">
                <div class="inspection-info-item">
                  <span class="inspection-info-label">当前轮次/总轮次</span>
                  <span class="inspection-info-value">{{ currentRoundText }}</span>
                </div>
                <div class="inspection-info-item">
                  <span class="inspection-info-label">当前巡检点/总巡检点</span>
                  <span class="inspection-info-value">{{ currentInspectionPointText }}</span>
                </div>
              </template>
            </div>
            <button
              v-if="inspectionRunning"
              class="inspection-action-btn danger"
              type="button"
              :disabled="inspectionActionLoading"
              @click="confirmStopInspection"
            >
              {{ inspectionActionLoading ? "处理中..." : "停止巡检" }}
            </button>
            <button
              v-else
              class="inspection-action-btn"
              type="button"
              :disabled="inspectionActionLoading"
              @click="confirmDispatchInspection"
            >
              {{ inspectionActionLoading ? "处理中..." : "下发巡检" }}
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
