Jelajahi Sumber

初步完成地图相关功能

jiuling 2 minggu lalu
induk
melakukan
f0afc25c70

+ 6 - 0
package.json

@@ -28,8 +28,12 @@
     "js-beautify": "1.15.4",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.3.2",
+    "mqtt": "^5.15.1",
     "nprogress": "0.2.0",
+    "ol": "9.2.4",
     "pinia": "3.0.4",
+    "sockjs-client": "1.6.1",
+    "stompjs": "2.3.3",
     "vue": "3.5.26",
     "vue-cropper": "1.1.1",
     "vue-router": "4.6.4",
@@ -37,7 +41,9 @@
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "5.2.4",
+    "sass": "1.32.13",
     "sass-embedded": "1.97.2",
+    "sass-loader": "10.1.1",
     "unplugin-auto-import": "0.18.6",
     "unplugin-vue-setup-extend-plus": "1.0.1",
     "vite": "6.4.1",

+ 73 - 0
src/api/map/task.js

@@ -0,0 +1,73 @@
+import request from '@/utils/request'
+
+// 查询任务列表
+export function listTask(mapName) {
+  return request({
+    url: `/v1/path/list`,
+    method: 'get',
+    params: { map: mapName },
+  })
+}
+
+// 查询任务详细
+export function getTask(mapName, taskName) {
+  return request({
+    url: `/v1/path`,
+    method: 'get',
+    params: { map: mapName, path: taskName },
+  })
+}
+
+// 新增任务
+export function addTask(data) {
+  return request({
+    url: '/v1/path',
+    method: 'post',
+    data: data,
+  })
+}
+
+// 更新任务
+export function updateTask(data) {
+  return request({
+    url: '/v1/path',
+    method: 'put',
+    data: data,
+  })
+}
+
+// 删除任务
+export function delTask(data) {
+  return request({
+    url: '/v1/path',
+    method: 'delete',
+    data: data,
+  })
+}
+
+// 更新任务状态
+export function updateTaskStatus(taskId, status) {
+  return request({
+    url: '/v1/path/status',
+    method: 'put',
+    params: { taskId, status },
+  })
+}
+
+// 获取任务执行记录
+export function getTaskLogs(params) {
+  return request({
+    url: '/v1/path/logs',
+    method: 'get',
+    params: params,
+  })
+}
+
+// 获取设备今日执行记录
+export function getTodayExecutionLogs(deviceId) {
+  return request({
+    url: '/v1/path/logs/today',
+    method: 'get',
+    params: { deviceId },
+  })
+}

+ 125 - 0
src/api/robot/control.js

@@ -0,0 +1,125 @@
+import request from '@/utils/request'
+
+// ==================== ASM功能操作 ====================
+
+/**
+ * 开始录制
+ */
+export function startRecord(deviceId, mapName) {
+  return request({
+    url: '/robot/control/asm/record/start',
+    method: 'post',
+    params: { deviceId, mapName }
+  })
+}
+
+/**
+ * 停止录制
+ */
+export function stopRecord(mapName) {
+  return request({
+    url: '/robot/control/asm/record/stop',
+    method: 'post',
+    params: {mapName}
+  })
+}
+
+/**
+ * 开始构建地图
+ * @param {string} deviceId 设备ID
+ * @param {string} mapName 地图名称
+ * @param {string} buildSteps 构建步骤(逗号分隔,如:recon,kfmix,octomap,tilemap,potree)
+ */
+export function startBuild(mapName, buildSteps) {
+  return request({
+    url: '/robot/control/asm/build/start',
+    method: 'post',
+    params: { mapName, buildSteps }
+  })
+}
+
+/**
+ * 停止构建地图
+ */
+export function stopBuild() {
+  return request({
+    url: '/robot/control/asm/build/stop',
+    method: 'post',
+    // params: {  }
+  })
+}
+
+/**
+ * 完成构建地图
+ */
+export function completedBuild(mapName, funcName) {
+  return request({
+    url: '/robot/control/asm/build/completed',
+    method: 'post',
+    params: { mapName, funcName }
+  })
+} 
+/**
+ * 开始实时SLAM
+ */
+export function startSlam(mapName) {
+  return request({
+    url: '/robot/control/asm/slam/start',
+    method: 'post',
+    params: {mapName }
+  })
+}
+
+/**
+ * 停止实时SLAM
+ */
+export function stopSlam() {
+  return request({
+    url: '/robot/control/asm/slam/stop',
+    method: 'post',
+    // params: {  }
+  })
+}
+
+// ==================== 标定操作 ====================
+
+/**
+ * 执行坐标系标定
+ */
+export function executeCalibration(mapName, calibrationPoints) {
+  return request({
+    url: '/robot/control/calibration/execute',
+    method: 'post',
+    params: {mapName },
+    data: calibrationPoints,
+    headers: {
+      'Content-Type': 'application/json'
+    }
+  })
+}
+
+// ==================== VSLAM操作 ====================
+
+/**
+ * 设置导航探索模式
+ */
+export function setNavExplore(autoMode) {
+  return request({
+    url: '/robot/control/vslam/navExplore',
+    method: 'post',
+    params: { autoMode }
+  })
+}
+
+// ==================== 状态查询 ====================
+
+/**
+ * 获取ASM操作状态
+ */
+export function getAsmStatus(functionName) {
+  return request({
+    url: '/robot/control/asm/status',
+    method: 'get',
+    params: {functionName }
+  })
+}

+ 316 - 0
src/api/robot/map.js

@@ -0,0 +1,316 @@
+import request from '@/utils/request'
+
+// ==================== 地图列表API ====================
+
+/**
+ * 获取地图列表
+ * 后端接口: GET /v1/map/list
+ * 返回格式: { status: true, maps: ['demo'], states: ['available'] }
+ */
+export function getMapList(deviceId) {
+  return request({
+    url: '/v1/map/list',
+    method: 'get',
+    params: { deviceId }
+  })
+}
+
+/**
+ * 获取当前使用中的地图
+ * 后端接口: GET /v1/map/using
+ * 返回格式: { status: true, name: '地图名' }
+ */
+export function getCurrentMap(deviceId) {
+  return request({
+    url: '/v1/map/using',
+    method: 'get',
+    params: { deviceId }
+  })
+}
+
+/**
+ * 获取地图缩略图
+ * 后端接口: GET /v1/map/thumbnail?map=地图名
+ * 返回格式: 图片二进制数据
+ */
+export function getMapThumbnail(mapName) {
+  return request({
+    url: '/v1/map/thumbnail',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+/**
+ * 重命名地图
+ * 后端接口: POST /v1/map/rename
+ * 请求格式: { map: '旧名称', rename: '新名称' }
+ * 返回格式: { status: true }
+ */
+export function renameMap(data) {
+  return request({
+    url: '/v1/map/rename',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 删除地图
+ * 后端接口: 需要确认删除接口路径
+ */
+export function deleteMap(data) {
+  return request({
+    url: '/v1/map/delete',
+    method: 'delete',
+    data
+  })
+}
+
+/**
+ * 获取地图组件列表
+ */
+export function getMapComponents(mapName) {
+  return request({
+    url: '/v1/map/components',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+/**
+ * 压缩导出地图
+ */
+export function compressMapExport(data) {
+  return request({
+    url: '/v1/map/export/compress',
+    method: 'post',
+    data,
+    timeout: 120000
+  })
+}
+
+/**
+ * 下载地图导出包
+ */
+export function downloadMapExport(mapName) {
+  return request({
+    url: '/v1/map/export',
+    method: 'get',
+    params: { map: mapName },
+    responseType: 'blob',
+    timeout: 60000
+  })
+}
+
+/**
+ * 导入地图
+ */
+export function importMap(formData) {
+  return request({
+    url: '/v1/map/import',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}
+
+// ==================== 路网数据API ====================
+
+/**
+ * 获取路网GeoJSON数据
+ * 后端接口: GET /v1/roadmap/geojson?map=地图名
+ * 返回格式: { status: true, data: {...} }
+ */
+export function getRoadMapGeoJson(mapName) {
+  return request({
+    url: '/v1/roadmap/geojson',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+/**
+ * 保存路网GeoJSON数据
+ * 后端接口: POST /v1/roadmap/geojson
+ */
+export function saveRoadMapGeoJson(data) {
+  return request({
+    url: '/v1/roadmap/geojson',
+    method: 'post',
+    data,
+    headers: {
+      'Content-Type': 'application/json'
+    }
+  })
+}
+
+// ==================== 任务路线API ====================
+
+/**
+ * 获取任务路线列表
+ */
+export function listPath(mapName) {
+  return request({
+    url: '/robot/map/path/list',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+/**
+ * 获取任务路线详情
+ */
+export function getPath(mapName, pathName) {
+  return request({
+    url: '/robot/map/path',
+    method: 'get',
+    params: { map: mapName, path: pathName }
+  })
+}
+
+/**
+ * 添加任务路线
+ */
+export function addPath(data) {
+  return request({
+    url: '/robot/map/path',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 删除任务路线
+ */
+export function deletePath(data) {
+  return request({
+    url: '/robot/map/path',
+    method: 'delete',
+    data
+  })
+}
+
+// ==================== 标定API ====================
+
+/**
+ * 获取标定历史
+ */
+export function getCalibrationHistory(mapName) {
+  return request({
+    url: '/robot/map/calibration/history',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+// ==================== VSLAM API ====================
+
+/**
+ * 获取VSLAM统计信息
+ */
+export function getVSlamStatistics(mapName) {
+  return request({
+    url: '/robot/map/vslam/statistics',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+/**
+ * 获取关键帧点云
+ */
+export function getKeyframeCloud(mapName, idx) {
+  return request({
+    url: `/robot/map/vslam/keyframe/cloud?map=${mapName}&idx=${idx}`,
+    method: 'get',
+    responseType: 'arraybuffer'
+  })
+}
+
+/**
+ * 获取关键帧变换矩阵
+ */
+export function getKeyframeTrans(mapName, idx) {
+  return request({
+    url: `/robot/map/vslam/keyframe/trans?map=${mapName}&idx=${idx}`,
+    method: 'get',
+    responseType: 'arraybuffer'
+  })
+}
+
+/**
+ * 获取闭环详情
+ */
+export function getClosureDetails(mapName, idx) {
+  return request({
+    url: '/robot/map/vslam/closure/details',
+    method: 'get',
+    params: { map: mapName, idx: idx }
+  })
+}
+
+// ==================== 传感器API ====================
+
+/**
+ * 获取点云数据
+ */
+export function getPointcloud() {
+  return request({
+    url: '/robot/map/sensor/pointcloud',
+    method: 'get',
+    responseType: 'arraybuffer'
+  })
+}
+
+// ==================== 地图瓦片API ====================
+
+/**
+ * 获取瓦片地图参数
+ * 后端接口: GET /v1/tilemap/details?map=地图名
+ */
+export function getTilemapDetails(mapName) {
+  return request({
+    url: '/v1/tilemap/details',
+    method: 'get',
+    params: { map: mapName }
+  })
+}
+
+/**
+ * 获取瓦片图片
+ * 后端接口: GET /v1/tilemap/tile/{mapName}/{zoom}/{x}/{y}
+ */
+export function getTilemapTile(mapName, zoom, x, y) {
+  return request({
+    url: `/v1/tilemap/tile/${mapName}/${zoom}/${x}/${y}`,
+    method: 'get',
+    responseType: 'blob'
+  })
+}
+
+// ==================== 设备状态API ====================
+
+/**
+ * 获取机器人实时位姿(从Redis缓存)
+ */
+export function getRobotPose(deviceId) {
+  return request({
+    url: '/robot/map/robot/pose',
+    method: 'get',
+    params: { deviceId }
+  })
+}
+
+/**
+ * 获取任务实时信息(从Redis缓存)
+ */
+export function getTaskRealtimeInfo(deviceId) {
+  return request({
+    url: '/robot/map/robot/task',
+    method: 'get',
+    params: { deviceId }
+  })
+}

+ 216 - 0
src/api/robot/mqtt.js

@@ -0,0 +1,216 @@
+import request from '@/utils/request'
+
+// ==================== MQTT消息发送API ====================
+// 对接 MqttController.java,提供HTTP接口调用MQTT消息发送
+
+/**
+ * 发送MQTT消息(通用接口)
+ */
+export function sendMqttMessage(topic, payload) {
+  return request({
+    url: '/mqtt/sendMessage',
+    method: 'post',
+    params: { topic, payload }
+  })
+}
+
+// ==================== LD导航系统接口 ====================
+
+/**
+ * 请求地图列表
+ */
+export function requestMapList() {
+  return request({
+    url: '/mqtt/ld/map/list',
+    method: 'post'
+  })
+}
+
+/**
+ * 位姿初始化(坐标)
+ */
+export function initPose(x, y, yaw) {
+  return request({
+    url: '/mqtt/ld/localization/init',
+    method: 'post',
+    params: {x, y, yaw }
+  })
+}
+
+/**
+ * 位姿初始化(路网点ID)
+ */
+export function initPoseByNid(nid) {
+  return request({
+    url: '/mqtt/ld/localization/initByNid',
+    method: 'post',
+    params: { nid }
+  })
+}
+
+/**
+ * 开启导航
+ */
+export function startNavigation(mapName) {
+  return request({
+    url: '/mqtt/ld/navigation/start',
+    method: 'post',
+    params: { mapName }
+  })
+}
+
+/**
+ * 关闭导航
+ */
+export function stopNavigation() {
+  return request({
+    url: '/mqtt/ld/navigation/stop',
+    method: 'post'
+  })
+}
+
+/**
+ * 重启导航
+ */
+export function restartNavigation(mapName) {
+  return request({
+    url: '/mqtt/ld/navigation/restart',
+    method: 'post',
+    params: { mapName }
+  })
+}
+
+/**
+ * 启动标准导航 (ASM.nav_standard.start)
+ */
+export function startNavStandard(mapName) {
+  return request({
+    url: '/mqtt/nav/standard/start',
+    method: 'post',
+    params: { mapName }
+  })
+}
+
+// ==================== 任务操作接口 ====================
+
+/**
+ * 前往目标点(路网点ID)
+ */
+export function gotoTargetByNid(mapName, nid) {
+  return request({
+    url: '/mqtt/ld/task/gotoByNid',
+    method: 'post',
+    params: { mapName, nid }
+  })
+}
+
+/**
+ * 前往目标点(坐标)
+ */
+export function gotoTargetByCoord( mapName, x, y) {
+  return request({
+    url: '/mqtt/ld/task/gotoByCoord',
+    method: 'post',
+    params: { mapName, x, y }
+  })
+}
+
+/**
+ * 暂停任务
+ */
+export function pauseTask() {
+  return request({
+    url: '/mqtt/ld/task/pause',
+    method: 'post'
+  })
+}
+
+/**
+ * 继续任务
+ */
+export function resumeTask() {
+  return request({
+    url: '/mqtt/ld/task/resume',
+    method: 'post'
+  })
+}
+
+/**
+ * 取消任务
+ */
+export function cancelTask() {
+  return request({
+    url: '/mqtt/ld/task/cancel',
+    method: 'post'
+  })
+}
+
+// ==================== 规划操作接口 ====================
+
+/**
+ * 遇障重规划
+ */
+export function replan() {
+  return request({
+    url: '/mqtt/ld/planning/replan',
+    method: 'post'
+  })
+}
+
+/**
+ * 请求路径规划
+ */
+export function requestPlanning(mapName, x, y) {
+  return request({
+    url: '/mqtt/planning/plan/request',
+    method: 'post',
+    params: { mapName, x, y }
+  })
+}
+
+/**
+ * 前往目标点(坐标) - 包含规划请求
+ */
+export function gotoTarget(mapName, x, y) {
+  return request({
+    url: '/mqtt/ld/task/gotoByCoord',
+    method: 'post',
+    params: { mapName, x, y }
+  })
+}
+
+/**
+ * 启动任务
+ */
+export function startTask( mapName, taskName) {
+  return request({
+    url: '/mqtt/task/procedure/start',
+    method: 'post',
+    params: { mapName, taskName }
+  })
+}
+
+// ==================== 急停操作接口 ====================
+
+/**
+ * 紧急停止
+ * @param {string} deviceId 设备ID
+ * @param {boolean} type true-急停, false-释放
+ */
+export function emergencyStop(type = true) {
+  return request({
+    url: '/mqtt/emergency/stop',
+    method: 'post',
+    params: {type }
+  })
+}
+
+/**
+ * 释放急停
+ */
+export function releaseEmergencyStop() {
+  return request({
+    url: '/mqtt/emergency/release',
+    method: 'post'
+  })
+}

+ 928 - 0
src/components/BottomInspector.vue

@@ -0,0 +1,928 @@
+<template>
+  <div 
+    v-if="visible" 
+    class="bottom-inspector is-editing"
+    :style="{ height: currentHeight }"
+    @keyup.esc="handleEscKey"
+    tabindex="-1"
+  >
+    <!-- 顶部居中拖拽条 -->
+    <div class="drag-section">
+      <div 
+        class="drag-handle" 
+        @mousedown="startResize"
+        @dblclick="resetHeight"
+      ></div>
+    </div>
+    
+    <!-- 头部标题栏 -->
+    <div class="inspector-header">
+      <div class="header-left">
+        <div class="title">当前元素参数</div>
+        <div v-if="elementData" class="element-info">
+          <el-tag size="mini" :type="getElementTypeTag(elementData.type)">
+            {{ getElementTypeName(elementData.type) }}
+          </el-tag>
+          <span class="element-id">{{ elementData.id }}</span>
+          <span v-if="elementData.name" class="element-name">{{ elementData.name }}</span>
+        </div>
+      </div>
+      <div class="header-right">
+        <!-- 快速操作 -->
+        <div class="quick-actions">
+          <el-tooltip content="定位到元素" placement="top">
+            <el-button 
+              size="mini" 
+              type="text" 
+              icon="el-icon-location"
+              @click="locateElement"
+            />
+          </el-tooltip>
+        </div>
+        <!-- 操作按钮 -->
+        <div class="action-buttons">
+          <el-button 
+            size="mini"
+            @click="handleCancel"
+          >
+            取消
+          </el-button>
+          <el-button 
+            size="mini" 
+            type="primary"
+            :loading="saving"
+            @click="handleSave"
+          >
+            保存
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 内容区域 -->
+    <div class="bottom-inspector__body" ref="contentRef">
+      <!-- 骨架屏 -->
+      <div v-if="loading" class="skeleton-content">
+        <div class="skeleton-section">
+          <div class="skeleton-title"></div>
+          <div class="skeleton-field"></div>
+          <div class="skeleton-field"></div>
+          <div class="skeleton-field"></div>
+        </div>
+        <div class="skeleton-section">
+          <div class="skeleton-title"></div>
+          <div class="skeleton-field"></div>
+          <div class="skeleton-field"></div>
+        </div>
+      </div>
+
+      <!-- 表单内容:左右两列布局 -->
+      <div v-else-if="elementData" class="inspector-sections">
+        <!-- 基础参数卡片 -->
+        <section class="inspector-card inspector-card--basic">
+          <div class="card-header">
+            <div class="section-header">
+              <span class="section-title">基础参数</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <el-form 
+              ref="basicFormRef"
+              :model="formData"
+              :rules="formRules"
+              label-width="100px"
+              size="small"
+              class="inspector-form"
+            >
+              <el-form-item label="元素ID" prop="id">
+                <el-input 
+                  v-model="formData.id" 
+                  readonly
+                  size="small"
+                />
+              </el-form-item>
+              <el-form-item label="元素名称" prop="name">
+                <el-input 
+                  v-model="formData.name" 
+                  placeholder="请输入元素名称"
+                  size="small"
+                />
+              </el-form-item>
+              
+              <!-- 坐标信息 -->
+              <template v-if="hasCoordinates">
+                <el-form-item label="X坐标(m)" prop="x">
+                  <el-input-number
+                    v-model="formData.x"
+                    :precision="3"
+                    :step="coordinateStep"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="Y坐标(m)" prop="y">
+                  <el-input-number
+                    v-model="formData.y"
+                    :precision="3"
+                    :step="coordinateStep"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item v-if="formData.z !== undefined" label="Z坐标(m)" prop="z">
+                  <el-input-number
+                    v-model="formData.z"
+                    :precision="3"
+                    :step="coordinateStep"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+              </template>
+
+              <!-- 线段/曲线基础参数 -->
+              <template v-if="elementData.type === 'LineString'">
+                <el-form-item label="起点ID" prop="startid">
+                  <el-input 
+                    v-model="formData.startid" 
+                    :readonly="true"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="终点ID" prop="endid">
+                  <el-input 
+                    v-model="formData.endid" 
+                    :readonly="true"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="方向控制" prop="directList">
+                  <div class="direction-controls">
+                    <el-checkbox
+                      v-model="formData.directList[0]"
+                      size="small"
+                    >
+                      起点→终点(前行)
+                    </el-checkbox>
+                    <el-checkbox
+                      v-model="formData.directList[1]"
+                      size="small"
+                    >
+                      起点→终点(倒行)
+                    </el-checkbox>
+                    <el-checkbox
+                      v-model="formData.directList[2]"
+                      size="small"
+                    >
+                      终点→起点(前行)
+                    </el-checkbox>
+                    <el-checkbox
+                      v-model="formData.directList[3]"
+                      size="small"
+                    >
+                      终点→起点(倒行)
+                    </el-checkbox>
+                  </div>
+                </el-form-item>
+                <el-form-item label="最大限速(m/s)" prop="maxspeed">
+                  <el-input-number
+                    v-model="formData.maxspeed"
+                    :min="0"
+                    :max="20"
+                    :precision="1"
+                    :step="0.5"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="最小限速(m/s)" prop="minspeed">
+                  <el-input-number
+                    v-model="formData.minspeed"
+                    :min="0"
+                    :precision="1"
+                    :step="0.5"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="车道宽度(m)" prop="lanewidth">
+                  <el-input-number
+                    v-model="formData.lanewidth"
+                    :min="0.1"
+                    :precision="2"
+                    :step="0.1"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="左车道数" prop="leftlanenum">
+                  <el-input-number
+                    v-model="formData.leftlanenum"
+                    :min="0"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="右车道数" prop="rightlanenum">
+                  <el-input-number
+                    v-model="formData.rightlanenum"
+                    :min="0"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+              </template>
+            </el-form>
+          </div>
+        </section>
+
+        <!-- 高级参数卡片 -->
+        <section class="inspector-card inspector-card--advanced">
+          <div class="card-header">
+            <div class="section-header">
+              <span class="section-title">高级参数</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <el-form 
+              ref="advancedFormRef"
+              :model="formData"
+              :rules="formRules"
+              label-width="100px"
+              size="small"
+              class="inspector-form"
+            >
+              <!-- 点位参数 -->
+              <template v-if="elementData.type === 'Point'">
+                <el-form-item label="航偏角(rad)" prop="yaw">
+                  <el-input-number
+                    v-model="formData.yaw"
+                    :precision="3"
+                    :step="0.1"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="航偏角使能" prop="isyawfix">
+                  <el-switch
+                    v-model="formData.isyawfix"
+                    size="small"
+                  />
+                </el-form-item>
+              </template>
+
+              <!-- 线段/曲线高级参数 -->
+              <template v-else-if="elementData.type === 'LineString'">
+                <el-form-item label="避障方式" prop="obstype">
+                  <el-select
+                    v-model="formData.obstype"
+                    size="small"
+                  >
+                    <el-option label="停车等待" :value="0" />
+                    <el-option label="车道绕障" :value="1" />
+                    <el-option label="路网绕障" :value="2" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="正向前移(m)" prop="s2eforward">
+                  <el-input-number
+                    v-model="formData.s2eforward"
+                    :precision="2"
+                    :step="0.1"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="逆向前移(m)" prop="e2sforward">
+                  <el-input-number
+                    v-model="formData.e2sforward"
+                    :precision="2"
+                    :step="0.1"
+                    controls-position="right"
+                    size="small"
+                  />
+                </el-form-item>
+              </template>
+
+              <!-- 多边形参数 -->
+              <template v-else-if="elementData.type === 'Polygon'">
+                <el-form-item label="区域类型" prop="type">
+                  <el-select
+                    v-model="formData.type"
+                    placeholder="请选择区域类型"
+                    size="small"
+                  >
+                    <el-option label="隔离区域" :value="0" />
+                    <el-option label="装饰区域" :value="1" />
+                    <el-option label="禁行区域" :value="2" />
+                    <el-option label="会车管制区" :value="3" />
+                    <el-option label="道闸管控区" :value="4" />
+                    <el-option label="GPS定位区" :value="11" />
+                    <el-option label="动态禁行区" :value="22" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="填充颜色" prop="color">
+                  <el-color-picker
+                    v-model="formData.color"
+                    size="small"
+                  />
+                </el-form-item>
+                <el-form-item label="透明度" prop="transparent">
+                  <el-slider
+                    v-model="formData.transparent"
+                    :min="0"
+                    :max="255"
+                    :step="5"
+                    size="small"
+                  />
+                </el-form-item>
+              </template>
+            </el-form>
+          </div>
+        </section>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="empty-state">
+        <i class="el-icon-info"></i>
+        <p>请选择一个元素查看其参数信息</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'BottomInspector',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    mode: {
+      type: String,
+      default: 'view'
+    },
+    data: {
+      type: Object,
+      default: null
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['edit', 'save', 'cancel', 'close', 'height-change', 'locate'],
+  data() {
+    return {
+      currentHeight: '20vh',
+      minHeight: 180,
+      maxHeight: '50vh',
+      isResizing: false,
+      startY: 0,
+      startHeight: 0,
+      
+      formData: {},
+      originalData: {},
+      
+      formRules: {
+        x: [
+          { required: true, message: '请输入X坐标', trigger: 'change' }
+        ],
+        y: [
+          { required: true, message: '请输入Y坐标', trigger: 'change' }
+        ]
+      },
+      
+      coordinateStep: 0.1,
+      
+      saving: false,
+      
+      hasUnsavedChanges: false
+    }
+  },
+  computed: {
+    elementData() {
+      return this.data
+    },
+    hasCoordinates() {
+      if (!this.elementData) return false
+      return this.elementData.type === 'Point' || 
+             this.elementData.position || 
+             (this.elementData.x !== undefined && this.elementData.y !== undefined)
+    }
+  },
+  watch: {
+    visible(newVal) {
+      if (newVal) {
+        this.$nextTick(() => {
+          this.$el?.focus()
+        })
+      }
+    },
+    data: {
+      handler(newData) {
+        if (newData) {
+          this.initFormData()
+        }
+      },
+      immediate: true,
+      deep: true
+    },
+    mode(newMode) {
+      if (newMode === 'edit') {
+        this.createSnapshot()
+      }
+    },
+    formData: {
+      handler() {
+        if (this.mode === 'edit') {
+          this.checkUnsavedChanges()
+        }
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    document.addEventListener('mousemove', this.handleMouseMove)
+    document.addEventListener('mouseup', this.handleMouseUp)
+  },
+  beforeDestroy() {
+    document.removeEventListener('mousemove', this.handleMouseMove)
+    document.removeEventListener('mouseup', this.handleMouseUp)
+  },
+  methods: {
+    startResize(event) {
+      this.isResizing = true
+      this.startY = event.clientY
+      const currentPx = this.$el.offsetHeight
+      this.startHeight = currentPx
+      event.preventDefault()
+    },
+    
+    handleMouseMove(event) {
+      if (!this.isResizing) return
+      
+      const deltaY = this.startY - event.clientY
+      let next = this.startHeight + deltaY
+      const max = Math.round(window.innerHeight * 0.5)
+      const min = 180
+      next = Math.max(min, Math.min(max, next))
+      
+      this.currentHeight = `${next}px`
+    },
+    
+    handleMouseUp() {
+      if (this.isResizing) {
+        this.isResizing = false
+        this.$emit('height-change', this.currentHeight)
+      }
+    },
+    
+    resetHeight() {
+      this.currentHeight = '20vh'
+      this.$emit('height-change', this.currentHeight)
+    },
+    
+    initFormData() {
+      if (!this.elementData) {
+        this.formData = {}
+        return
+      }
+      
+      const data = { ...this.elementData }
+      
+      if (data.position && Array.isArray(data.position)) {
+        data.x = data.position[0]
+        data.y = data.position[1]
+        data.z = data.position[2] || 0
+      }
+      
+      if (data.direct !== undefined && data.type === 'LineString') {
+        const value = data.direct - 100 || 0
+        const binary = value.toString(2).padStart(4, '0')
+        data.directList = [
+          binary[0] === '1',
+          binary[1] === '1', 
+          binary[2] === '1',
+          binary[3] === '1'
+        ]
+      }
+      
+      if (this.elementData.type === 'Polygon') {
+        if (data.type === undefined || data.type === 'Polygon') {
+          data.type = 1
+        }
+      }
+      
+      this.formData = { ...data }
+      this.hasUnsavedChanges = false
+    },
+    
+    createSnapshot() {
+      this.originalData = JSON.parse(JSON.stringify(this.formData))
+    },
+    
+    restoreSnapshot() {
+      if (this.originalData) {
+        this.formData = JSON.parse(JSON.stringify(this.originalData))
+        this.hasUnsavedChanges = false
+      }
+    },
+    
+    checkUnsavedChanges() {
+      if (!this.originalData) return
+      
+      this.hasUnsavedChanges = JSON.stringify(this.formData) !== JSON.stringify(this.originalData)
+    },
+    
+    handleSave() {
+      const basicFormValid = this.$refs.basicFormRef ? this.$refs.basicFormRef.validate() : Promise.resolve(true)
+      const advancedFormValid = this.$refs.advancedFormRef ? this.$refs.advancedFormRef.validate() : Promise.resolve(true)
+      
+      Promise.all([basicFormValid, advancedFormValid]).then((results) => {
+        const allValid = results.every(valid => valid === true)
+        
+        if (allValid) {
+          this.saving = true
+          
+          const saveData = { ...this.formData }
+          
+          if (saveData.directList && this.elementData.type === 'LineString') {
+            const binaryString = saveData.directList.map(bit => (bit ? '1' : '0')).join('')
+            const value = parseInt(binaryString, 2)
+            saveData.direct = value + 100
+            delete saveData.directList
+          }
+          
+          if (saveData.x !== undefined && saveData.y !== undefined) {
+            saveData.position = [saveData.x, saveData.y, saveData.z || 0]
+          }
+          
+          this.$emit('save', saveData)
+          
+          this.saving = false
+          this.hasUnsavedChanges = false
+        } else {
+          this.$message.error('请检查表单数据')
+        }
+      }).catch(() => {
+        this.$message.error('表单验证失败')
+      })
+    },
+    
+    handleCancel() {
+      if (this.hasUnsavedChanges) {
+        this.$confirm('是否放弃未保存的更改?', '确认', {
+          confirmButtonText: '放弃更改',
+          cancelButtonText: '继续编辑',
+          type: 'warning'
+        }).then(() => {
+          this.restoreSnapshot()
+          this.$emit('cancel')
+        })
+      } else {
+        this.$emit('cancel')
+      }
+    },
+    
+    handleClose() {
+      if (this.mode === 'edit' && this.hasUnsavedChanges) {
+        this.$confirm('当前有未保存的更改,是否放弃?', '确认关闭', {
+          confirmButtonText: '放弃更改',
+          cancelButtonText: '继续编辑',
+          type: 'warning'
+        }).then(() => {
+          this.$emit('close')
+        })
+      } else {
+        this.$emit('close')
+      }
+    },
+    
+    handleEscKey() {
+      if (!this.hasUnsavedChanges) {
+        this.handleClose()
+      }
+    },
+    
+    locateElement() {
+      this.$emit('locate', this.elementData)
+      this.$message.success(`已定位到元素 ${this.elementData.id}`)
+    },
+    
+    getElementTypeTag(type) {
+      const typeMap = {
+        'Point': 'success',
+        'LineString': 'primary', 
+        'Polygon': 'warning'
+      }
+      return typeMap[type] || 'info'
+    },
+    
+    getElementTypeName(type) {
+      const typeMap = {
+        'Point': '点',
+        'LineString': '线',
+        'Polygon': '面'
+      }
+      return typeMap[type] || '未知'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.bottom-inspector {
+  position: absolute;
+  left: var(--left-safe);
+  width: calc(100% - var(--left-safe) - var(--right-safe));
+  bottom: 0;
+  
+  min-height: 280px;
+  height: 20vh;
+  max-height: 50vh;
+  background: #fff;
+  border-radius: 12px 12px 0 0;
+  box-shadow: 0 8px 28px rgba(0,0,0,0.12);
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  
+  z-index: 9;
+  pointer-events: auto;
+  overflow: hidden;
+
+  &:focus {
+    outline: none;
+  }
+  
+  &.is-editing {
+    border-top-color: #409eff;
+    box-shadow: 0 8px 32px rgba(64, 158, 255, 0.2);
+  }
+}
+
+.drag-section {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 8px 0;
+  background: #fff;
+  flex-shrink: 0;
+  
+  .drag-handle {
+    width: 56px;
+    height: 4px;
+    border-radius: 2px;
+    background: rgba(0,0,0,.18);
+    cursor: ns-resize;
+    transition: background-color 0.2s ease;
+    
+    &:hover {
+      background: rgba(0,0,0,.28);
+    }
+  }
+}
+
+.inspector-header {
+  flex: 0 0 auto;
+  height: 44px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 16px;
+  border-bottom: 1px solid #e4e7ed;
+  background: #fafbfc;
+  box-sizing: border-box;
+  
+  .header-left {
+    display: flex;
+    align-items: center;
+    flex: 1;
+    
+    .title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+      margin-right: 16px;
+    }
+    
+    .element-info {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      color: #606266;
+      font-size: 14px;
+      
+      .element-id {
+        font-family: 'Monaco', 'Menlo', monospace;
+        background: #f0f2f5;
+        padding: 2px 6px;
+        border-radius: 4px;
+        font-size: 12px;
+      }
+      
+      .element-name {
+        color: #909399;
+      }
+    }
+  }
+  
+  .header-right {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+  
+  .quick-actions {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    margin-right: 8px;
+    padding-right: 8px;
+    border-right: 1px solid #e4e7ed;
+  }
+  
+  .action-buttons {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+.bottom-inspector__body {
+  flex: 1 1 auto;
+  overflow-y: auto;
+  padding: 12px 16px;
+  box-sizing: border-box;
+}
+
+.inspector-sections {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+  align-items: start;
+}
+
+.inspector-card {
+  background: #fff;
+  border: 1px solid #eef0f3;
+  border-radius: 10px;
+  overflow: hidden;
+}
+
+.inspector-card .card-header {
+  position: sticky; 
+  top: 0;
+  background: #f8fafc; 
+  z-index: 1;
+  padding: 10px 12px; 
+  border-bottom: 1px solid #eef0f3;
+}
+
+.inspector-card .card-body {
+  padding: 12px;
+}
+
+.inspector-card :deep(.el-form),
+.inspector-card :deep(.el-form-item),
+.inspector-card :deep(.el-input),
+.inspector-card :deep(.el-input__inner) { 
+  width: 100%; 
+}
+
+.bottom-inspector .el-form,
+.bottom-inspector .el-form-item,
+.bottom-inspector .el-input,
+.bottom-inspector .el-input__inner {
+  width: 100%;
+  max-width: 100%;
+}
+
+.inspector-form {
+  ::v-deep .el-form-item {
+    margin-bottom: 12px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    .el-form-item__label {
+      font-size: 13px;
+      line-height: 1.4;
+      padding-bottom: 4px;
+    }
+    
+    .el-form-item__content {
+      line-height: 1.6;
+    }
+  }
+  
+  .direction-controls {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    
+    .el-checkbox {
+      margin-right: 0;
+    }
+  }
+}
+
+.skeleton-content {
+  .skeleton-section {
+    margin-bottom: 24px;
+    
+    .skeleton-title {
+      height: 16px;
+      background: linear-gradient(90deg, #f0f2f5 25%, #e6e8eb 50%, #f0f2f5 75%);
+      background-size: 200% 100%;
+      animation: skeleton-loading 1.5s infinite;
+      border-radius: 4px;
+      margin-bottom: 16px;
+      width: 120px;
+    }
+    
+    .skeleton-field {
+      height: 32px;
+      background: linear-gradient(90deg, #f0f2f5 25%, #e6e8eb 50%, #f0f2f5 75%);
+      background-size: 200% 100%;
+      animation: skeleton-loading 1.5s infinite;
+      border-radius: 4px;
+      margin-bottom: 12px;
+      
+      &:nth-child(2) { animation-delay: 0.1s; }
+      &:nth-child(3) { animation-delay: 0.2s; }
+      &:nth-child(4) { animation-delay: 0.3s; }
+    }
+  }
+}
+
+@keyframes skeleton-loading {
+  0% {
+    background-position: 200% 0;
+  }
+  100% {
+    background-position: -200% 0;
+  }
+}
+
+.empty-state {
+  text-align: center;
+  padding: 40px 20px;
+  color: #909399;
+  
+  i {
+    font-size: 48px;
+    margin-bottom: 16px;
+    display: block;
+    color: #c0c4cc;
+  }
+  
+  p {
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+@media (max-width: 992px) {
+  .bottom-inspector {
+    left: 12px;
+    width: calc(100% - 24px);
+    
+    .inspector-header {
+      .header-left .title {
+        font-size: 14px;
+        margin-right: 12px;
+      }
+      
+      .element-info {
+        display: none;
+      }
+      
+      .quick-actions {
+        display: none;
+      }
+    }
+    
+    .bottom-inspector__body {
+      padding: 12px;
+    }
+    
+    .inspector-sections {
+      grid-template-columns: 1fr;
+      gap: 12px;
+    }
+    
+    .inspector-card .card-body {
+      padding: 10px;
+    }
+    
+    .inspector-form {
+      ::v-deep .el-form-item .el-form-item__label {
+        font-size: 12px;
+      }
+    }
+  }
+}
+</style>

+ 2086 - 0
src/components/OlMap/index.vue

@@ -0,0 +1,2086 @@
+<template>
+  <div class="map-wrapper">
+    <div class="mapParent" id="mapParent"
+      :style="{ backgroundColor: backgroundColor, lineHeight: height }">
+      <span class="maptext">
+        <span v-if="textShow">请选择地图</span>
+        <br>
+      </span>
+    </div>
+    <div id="tooltip" class="tooltip">
+      <div class="tooltip-header">
+        <i class="el-icon-location"></i>
+        <span class="tooltip-title">添加目标点</span>
+      </div>
+      <div id="tooltip-content" class="tooltip-content"></div>
+      <div class="tooltip-actions">
+        <el-button type="primary" size="small" @click="addNowPoint" class="action-btn">
+          <i class="el-icon-check"></i>
+          添加点位
+        </el-button>
+        <el-button size="small" @click="clearNowPoint" class="action-btn">
+          <i class="el-icon-close"></i>
+          取消添加
+        </el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue'
+import 'ol/ol.css'
+import Map from 'ol/Map'
+import View from 'ol/View'
+import Overlay from 'ol/Overlay';
+import TileLayer from 'ol/layer/Tile'
+import VectorLayer from 'ol/layer/Vector'
+import VectorSource from 'ol/source/Vector'
+import XYZ from 'ol/source/XYZ'
+import Projection from 'ol/proj/Projection'
+import GeoJSON from 'ol/format/GeoJSON'
+import Feature from 'ol/Feature'
+import Point from 'ol/geom/Point'
+import SrcXYZ from "ol/source/XYZ";
+import { fromLonLat } from 'ol/proj'
+import MultiPoint from 'ol/geom/MultiPoint'
+import LineString from 'ol/geom/LineString'
+import { Style, Stroke, Fill, Circle, Icon, Text } from 'ol/style'
+import { defaults as defaultControls } from 'ol/control'
+import { defaults as defaultInteractions, DragRotateAndZoom, Draw, Modify, Snap } from 'ol/interaction'
+import { easeOut, inAndOut } from 'ol/easing'
+import { getTilemapDetails, getRoadMapGeoJson, getPointcloud } from '@/api/robot/map'
+import { ElMessage } from 'element-plus'
+
+// 地图文件地址url
+const fileUrl = import.meta.env.VUE_APP_MAP_FILE_URL || 'http://192.168.0.102:8086'
+
+// 计算属性:地图服务地址
+const url = computed(() => {
+  return fileUrl
+})
+
+const props = defineProps({
+  mapName: {
+    type: String,
+    required: true
+  },
+  pointSwitchShow: {
+    type: Boolean,
+    default: true
+  },
+  pointSwitchStatus: {
+    type: Boolean,
+    default: false
+  },
+  textShow: {
+    type: Boolean,
+    default: true
+  },
+  content: {
+    type: String,
+    default: ""
+  },
+  backgroundColor: {
+    type: String,
+    default: "#d5d8e1"
+  },
+  height: {
+    type: String,
+    default: "365px"
+  },
+  width: {
+    type: String,
+    default: "365px"
+  },
+  zoom: {
+    type: Number,
+    default: 2
+  },
+  showDefaultControls: {
+    type: Boolean,
+    default: true
+  },
+  minZoom: {
+    type: Number,
+    default: 1
+  },
+  pointSwitch: {
+    type: Boolean,
+    default: false
+  },
+  baseLayerShow: {
+    type: Boolean,
+    default: true
+  },
+  pointSelectionEnabled: {
+    type: Boolean,
+    default: false
+  },
+  pointDraSelectEnabled: {
+    type: Boolean,
+    default: false
+  },
+  poseInitEnable: {
+    type: Boolean,
+    default: false
+  },
+  pointEvent: {
+    type: Function,
+    default: () => { }
+  },
+  robotPoseData: {
+    type: Object,
+    required: false,
+    default: function () {
+      return { x: 0, y: 0, angle: 0 }
+    }
+  },
+  existPointShow: {
+    type: Boolean,
+    default: false
+  },
+  routeIndex: {
+    type: Number,
+    default: -1
+  },
+  isRobotFollow: {
+    type: Boolean,
+    default: false
+  },
+  poseCalibrationIndex: {
+    type: Number,
+    default: 0
+  },
+  isShowRobot: {
+    type: Boolean,
+    default: true
+  },
+  showPointcloud: {
+    type: Boolean,
+    default: false
+  },
+  showRoadNetwork: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const emit = defineEmits([
+  'addNowPoint',
+  'elementRoadInitEnd',
+  'elementRoadDrawEnd',
+  'removeElementResult',
+  'selectShowEleResult',
+  'initNavigationResult',
+  'point-cloud-load-end',
+  'togglePointSelection'
+])
+
+// ==================== 响应式数据 ====================
+const map = ref(null)
+const config_json = ref("")
+const geojson_data = ref("")
+const robot_src = ref(null)
+const pose = ref({ x: 0, y: 0, angle: 0 })
+const oldRobotCode = ref("")
+const baseLayer = ref(null)
+const pointAll = ref([])
+const currentPlace = ref([])
+const currentCoordinate = ref([])
+const maxIndex = ref(0)
+const icon_src = ref(null)
+const roadmap_src = ref(null)
+const roadmapLayer = ref(null)
+const maxPointIdNum = ref(0)
+const maxLineIdNum = ref(0)
+const maxBowNum = ref(0)
+const maxPolygonNum = ref(0)
+const selectedFeatureId = ref('')
+const previewFeature = ref(null)
+const arrowFeature = ref(null)
+const calibrationList = ref([])
+const trajectorySource = ref(null)
+const currentTrajectory = ref(null)
+const trajectoryProgress = ref(0)
+const pointcloudLayer = ref(null)
+const pointcloudTimer = ref(null)
+const aiAlarmList = ref([])
+
+// Drawing tools
+const draw = ref(null)
+const snap = ref(null)
+const modify = ref(null)
+
+// Icons
+const icon = ref({
+  mark: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+    <svg width="22" height="22" xmlns="http://www.w3.org/2000/svg">
+      <circle cx="11" cy="11" r="10" fill="#4096BD" stroke="white" stroke-width="2"/>
+    </svg>
+  `),
+  point: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+    <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg">
+      <circle cx="10" cy="10" r="8" fill="#00BFFF" stroke="white" stroke-width="2"/>
+    </svg>
+  `),
+  robot: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+    <svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
+      <polygon points="20,2 38,35 20,28 2,35" fill="#FF5722" stroke="#fff" stroke-width="2"/>
+    </svg>
+  `)
+})
+
+// ==================== 生命周期 ====================
+onMounted(() => {
+  nextTick(() => {
+    loadMap()
+  })
+})
+
+onBeforeUnmount(() => {
+  stopPointcloudUpdate()
+  if (map.value) {
+    map.value.setTarget(null)
+  }
+})
+
+// ==================== 监听器 ====================
+watch(() => props.baseLayerShow, (newVal) => {
+  if (newVal !== undefined && baseLayer.value) {
+    console.log("newVal:", newVal);
+        console.log("baseLayer:", baseLayer.value);
+
+    baseLayer.value.setVisible(newVal)
+  }
+})
+
+watch(() => props.pointSelectionEnabled, (newVal) => {
+  const mapElement = document.querySelector('.tileMap')
+  if (mapElement) {
+    mapElement.style.cursor = newVal ? 'pointer' : 'default'
+  }
+})
+
+watch(() => props.pointDraSelectEnabled, (newVal) => {
+  const mapElement = document.querySelector('.tileMap')
+  if (mapElement) {
+    mapElement.style.cursor = newVal ? 'pointer' : 'default'
+  }
+})
+
+watch(() => props.isRobotFollow, (newVal) => {
+  if (newVal) {
+    followRotation()
+  } else if (map.value) {
+    map.value.getView().setRotation(0)
+  }
+})
+
+watch(() => props.robotPoseData, (newVal) => {
+  if (newVal) {
+    robotPoseUpdateCallback(newVal.x, newVal.y, newVal.angle)
+  }
+}, { deep: true })
+
+watch(() => props.poseCalibrationIndex, (index) => {
+  if (index < 0) {
+    const id = Math.abs(index)
+    removeCalibrationById(id)
+  } else {
+    const newId = calibrationList.value.length + 1
+    const point = { id: newId, x: props.robotPoseData.x, y: props.robotPoseData.y }
+    calibrationList.value.push(point)
+    refreshCalibrationOverlays()
+  }
+})
+
+watch(() => props.poseInitEnable, (newVal) => {
+  const mapElement = document.querySelector('.tileMap')
+  if (mapElement) {
+    mapElement.style.cursor = newVal ? 'pointer' : 'default'
+  }
+})
+
+watch(() => props.showPointcloud, (newVal) => {
+  if (pointcloudLayer.value) {
+    pointcloudLayer.value.setVisible(newVal)
+    if (newVal) {
+      startPointcloudUpdate()
+    } else {
+      stopPointcloudUpdate()
+      const features = pointcloudLayer.value.getSource().getFeatures()
+      if (features.length > 0) {
+        features[0].setGeometry(new MultiPoint([]))
+      }
+    }
+  }
+})
+
+watch(() => props.showRoadNetwork, (newVal) => {
+  if (roadmapLayer.value) {
+    roadmapLayer.value.setVisible(newVal)
+  }
+})
+
+// 监听地图名称变化,重新加载地图
+watch(() => props.mapName, (newMapName) => {
+  if (newMapName) {
+    nextTick(() => {
+      loadMap()
+    })
+  }
+})
+
+// 监听地图尺寸变化,重新设置地图大小
+watch([() => props.width, () => props.height], () => {
+  nextTick(() => {
+    updateMapSize()
+  })
+})
+
+// ==================== 方法 ====================
+
+// 点击添加点位的操作
+function addNowPoint() {
+  emit('addNowPoint', currentCoordinate.value, currentPlace.value)
+  hideToop()
+  addHtmlIcon(currentCoordinate.value[0], currentCoordinate.value[1], currentCoordinate.value[2], icon.value.mark, "pose")
+}
+
+// 点击取消点位的操作
+function clearNowPoint() {
+  hideToop()
+  currentCoordinate.value = []
+  currentPlace.value = []
+}
+
+// 加载地图数据
+async function loadMap() {
+  console.log("[OlMap] loadMap 开始, mapName:", props.mapName)
+
+  if (!props.mapName) {
+    setMapText('请选择地图')
+    return
+  }
+
+  // 清理旧地图数据
+  if (map.value) {
+    map.value.setTarget(null)
+    map.value = null
+  }
+  if (roadmap_src.value) {
+    roadmap_src.value.clear()
+    roadmap_src.value = null
+  }
+  roadmapLayer.value = null
+  config_json.value = ""
+  geojson_data.value = ""
+  selectedFeatureId.value = ''
+  maxPointIdNum.value = 0
+  maxLineIdNum.value = 0
+  maxBowNum.value = 0
+  maxPolygonNum.value = 0
+  calibrationList.value = []
+
+  try {
+    const configRes = await getTilemapDetails(props.mapName)
+    config_json.value = configRes
+
+    const geojsonRes = await getRoadMapGeoJson(props.mapName)
+    // Handle both direct geojson and wrapped response
+    geojson_data.value = geojsonRes.data || geojsonRes
+    console.log("初始化");
+    initMapHaveDate()
+    initRoad(geojson_data.value)
+  } catch (e) {
+    setMapText('加载地图错误')
+    console.error('加载地图失败:', e)
+  }
+}
+
+// 设置中心的文字
+function setMapText(str = '') {
+  str = str == "" ? '加载地图错误' : str
+  const obj = document.getElementById("mapParent")
+  if (obj) {
+    obj.innerHTML = `<span class='maptext' style='font-size: 20px;color: grey;height: 100%'>${str}</span>`
+  }
+}
+
+// 初始化地图(有数据)
+function initMapHaveDate() {
+  
+  if (!config_json.value || config_json.value == "") return
+
+  const mapNode = document.getElementById("tileMap")
+  if (mapNode) {
+    document.getElementById("mapParent").removeChild(mapNode)
+  }
+
+  const parent = document.getElementById("mapParent")
+  const div = document.createElement('div')
+  div.id = 'tileMap'
+  div.className = 'tileMap'
+  parent.innerHTML = ''
+  parent.appendChild(div)
+
+  initMap(config_json.value)
+
+  if (props.isShowRobot) {
+    initRobot()
+  }
+}
+
+// 初始化地图
+function initMap(config) {
+  if (!config || config == "") {
+    console.warn("[OlMap] initMap: config 为空")
+    return
+  }
+
+  // console.log("[OlMap] initMap 开始, config:", JSON.stringify(config, null, 2))
+
+  const tile_param = {
+    layer_cnt: config.layer_cnt,
+    min_zoom: config.min_zoom,
+    max_zoom: config.max_zoom,
+    projection: config.projection
+  }
+
+  console.log("[OlMap] tile_param:", tile_param)
+
+  let startPoint = null
+  let isDrawing = false
+
+  map.value = new Map({
+    target: "tileMap",
+    layers: [],
+    view: null,
+    interactions: defaultInteractions({
+      dragRotate: false,
+      dragZoom: false
+    }),
+    controls: props.showDefaultControls ? defaultControls() : []
+  })
+
+  // Map click event
+  map.value.on('click', (evt) => {
+    console.log("执行点击");
+    
+    // 点位选择模式
+    if (props.pointSelectionEnabled) {
+      let feature = map.value.forEachFeatureAtPixel(evt.pixel, (f) => f)
+      let xy
+      if (feature && (feature.values_.id || feature.getId()) && feature.getGeometry().getType() === 'Point') {
+        xy = feature.getGeometry().getCoordinates()
+      } else {
+        xy = evt.coordinate
+      }
+
+      const tooltipContent = document.getElementById('tooltip-content')
+      tooltipContent.innerHTML = ''
+      tooltipContent.innerHTML += `<p>确认在此位置添加目标点?</p>`
+      tooltipContent.innerHTML += `<p>X: ${xy[0].toFixed(3)}, Y: ${xy[1].toFixed(3)}</p>`
+
+      const tooltip = document.getElementById('tooltip')
+      tooltip.style.display = 'block'
+      tooltip.style.left = (evt.pixel[0] + 70) + 'px'
+      tooltip.style.top = evt.pixel[1] + 'px'
+
+      maxIndex.value++
+      currentCoordinate.value = [maxIndex.value, xy[0], xy[1], xy[2] || 0]
+      currentPlace.value = evt.pixel
+    }
+
+    // 位姿初始化模式
+    if (props.poseInitEnable) {
+      let feature = map.value.forEachFeatureAtPixel(evt.pixel, (f) => f)
+      let coord
+      let nid = feature && feature.getId ? feature.getId() : null
+
+      if (feature && feature.getGeometry().getType() === 'Point') {
+        coord = feature.getGeometry().getCoordinates()
+      } else {
+        coord = evt.coordinate
+      }
+
+      if (!isDrawing) {
+        startPoint = coord
+        isDrawing = true
+        ElMessage({ message: '起点已设置,请点击第二个位置确定机器人朝向(或右键取消)', type: 'info', duration: 3000 })
+      } else {
+        completeDrawing(startPoint, coord, nid)
+        if (arrowFeature.value) {
+          roadmap_src.value.removeFeature(arrowFeature.value)
+          arrowFeature.value = null
+        }
+        if (previewFeature.value) {
+          roadmap_src.value.removeFeature(previewFeature.value)
+          previewFeature.value = null
+        }
+        isDrawing = false
+        startPoint = null
+      }
+    }
+
+    // 编辑页面元素选择模式
+    if (props.pointDraSelectEnabled) {
+      let feature = map.value.forEachFeatureAtPixel(evt.pixel, (f) => f)
+      editElementSelectHandle(feature)
+    }
+  })
+
+  // Mouse move for pose init preview
+  map.value.on('pointermove', (evt) => {
+    if (props.poseInitEnable && isDrawing && startPoint) {
+      const endPoint = evt.coordinate
+
+      if (!previewFeature.value) {
+        const line = new LineString([startPoint, endPoint])
+        previewFeature.value = new Feature(line)
+        previewFeature.value.setStyle(new Style({
+          stroke: new Stroke({ color: 'rgba(255, 50, 50, 0.8)', width: 5, lineDash: [10, 5] })
+        }))
+        roadmap_src.value.addFeature(previewFeature.value)
+
+        arrowFeature.value = new Feature({ geometry: new LineString([startPoint, endPoint]) })
+        arrowFeature.value.setStyle(new Style({
+          renderer: (pixelCoordinates, state) => {
+            const context = state.context
+            const [startX, startY] = pixelCoordinates[0]
+            const [endX, endY] = pixelCoordinates[1]
+            const dx = endX - startX
+            const dy = endY - startY
+            const length = Math.sqrt(dx * dx + dy * dy)
+            if (length < 10) return
+
+            const angle = Math.atan2(dy, dx)
+            const arrowLength = 25
+            const arrowAngle = Math.PI / 6
+            const tipX = endX, tipY = endY
+            const leftX = tipX - arrowLength * Math.cos(angle - arrowAngle)
+            const leftY = tipY - arrowLength * Math.sin(angle - arrowAngle)
+            const rightX = tipX - arrowLength * Math.cos(angle + arrowAngle)
+            const rightY = tipY - arrowLength * Math.sin(angle + arrowAngle)
+
+            context.beginPath()
+            context.moveTo(tipX, tipY)
+            context.lineTo(leftX, leftY)
+            context.lineTo(rightX, rightY)
+            context.closePath()
+            context.fillStyle = 'rgba(255, 50, 50, 0.9)'
+            context.fill()
+            context.strokeStyle = 'rgba(200, 0, 0, 1)'
+            context.lineWidth = 2
+            context.stroke()
+
+            context.beginPath()
+            context.arc(startX, startY, 6, 0, 2 * Math.PI)
+            context.fillStyle = 'rgba(50, 150, 255, 0.8)'
+            context.fill()
+            context.strokeStyle = 'rgba(0, 100, 200, 1)'
+            context.lineWidth = 2
+            context.stroke()
+          }
+        }))
+        roadmap_src.value.addFeature(arrowFeature.value)
+      } else {
+        previewFeature.value.getGeometry().setCoordinates([startPoint, endPoint])
+        arrowFeature.value.getGeometry().setCoordinates([startPoint, endPoint])
+      }
+    }
+  })
+
+  // Right-click to cancel pose init
+  map.value.on('contextmenu', (evt) => {
+    if (props.poseInitEnable && isDrawing) {
+      evt.preventDefault()
+      if (previewFeature.value) {
+        roadmap_src.value.removeFeature(previewFeature.value)
+        previewFeature.value = null
+      }
+      if (arrowFeature.value) {
+        roadmap_src.value.removeFeature(arrowFeature.value)
+        arrowFeature.value = null
+      }
+      isDrawing = false
+      startPoint = null
+      ElMessage({ message: '位姿初始化已取消', type: 'warning', duration: 2000 })
+    }
+  })
+
+  // 从配置获取地图参数
+  const extent = tile_param.projection.extent
+  const centerX = (extent[0] + extent[2]) / 2  // 地图X中心
+  const centerY = (extent[1] + extent[3]) / 2  // 地图Y中心
+
+  // 创建投影
+  const projection = new Projection({
+    extent: extent
+  })
+
+  // Set view - 居中显示整个地图
+  map.value.setView(new View({
+    projection: projection,
+    center: [centerX, centerY],
+    zoom: tile_param.min_zoom,
+    minZoom: tile_param.min_zoom,
+    maxZoom: tile_param.max_zoom
+  }))
+// 在 initMap 函数中,在创建 XYZ source 之前添加
+const originalTileUrl = `/v1/tilemap/tile/${props.mapName}/{z}/{x}/{y}`
+console.log('[调试] 瓦片URL模板:', originalTileUrl)
+
+
+  // Create base tile layer - use proxy path to avoid CORS issues
+  // URL格式符合OSM标准: {z}/{x}/{y} -> 后端接口: /v1/tilemap/tile/{mapName}/{z}/{x}/{y}
+  const tileUrl = `/v1/tilemap/tile/${props.mapName}/{z}/{x}/{y}`
+  // const tileUrl = `http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}`
+  console.log("[OlMap] 初始化瓦片图层, URL:", tileUrl)
+  console.log("[OlMap] mapName:", props.mapName)
+  console.log("[OlMap] tile_param:", tile_param)
+  console.log("[OlMap] extent:", extent)
+
+  baseLayer.value = new TileLayer({
+    source: new XYZ({
+      url: tileUrl,
+      projection: projection,
+      wrapX: false,
+      extent: extent,
+    })
+  })
+// 在创建 TileLayer 后,监听 tile loadstart 事件
+baseLayer.value.getSource().on('tileloadstart', function(event) {
+  const coord = event.tile.tileCoord
+  console.log('coord', coord)
+  if (coord) {
+    console.log('[瓦片请求]', {
+      z: coord[0],
+      x: coord[1],
+      y: coord[2],
+      url: event.tile.src_
+    })
+  }
+})
+  baseLayer.value.getSource().on('tileloadstart', function(event) {
+    console.log('开始加载瓦片')
+  })
+  // 监听瓦片加载错误
+  baseLayer.value.getSource().on('tileloaderror', function(event) {
+    console.error("[OlMap] 瓦片加载失败:", event)
+  })
+
+  // 监听瓦片加载成功
+  baseLayer.value.getSource().on('tileloadend', function(event) {
+    console.log("[OlMap] 瓦片加载成功:", event.tile.coordinate)
+  })
+
+  console.log("baseLayer initialized:", baseLayer.value);
+  console.log(document.getElementById('tileMap'))
+  map.value.addLayer(baseLayer.value)
+
+  // Initialize point cloud layer
+  initPointcloudLayer()
+
+  // Ensure map takes full size of parent container
+  map.value.updateSize()
+}
+
+// 初始化路网
+function initRoad(geojson) {
+  if (!geojson || geojson === "") return
+
+  if (roadmap_src.value && roadmap_src.value.getFeatures().length > 0) {
+    console.warn('保护模式: 地图已有数据,禁止重新加载')
+    return
+  }
+
+  if (roadmap_src.value) {
+    roadmap_src.value.clear()
+    if (roadmapLayer.value) {
+      roadmapLayer.value.setVisible(props.showRoadNetwork)
+    }
+  } else {
+    roadmap_src.value = new VectorSource()
+    roadmapLayer.value = new VectorLayer({
+      source: roadmap_src.value,
+      style: customizedStyle,
+      visible: props.showRoadNetwork
+    })
+    map.value.addLayer(roadmapLayer.value)
+  }
+
+  const features = new GeoJSON().readFeatures(geojson)
+
+  features.forEach(feature => {
+    const innerId = feature.values_.id
+    if (!feature.getId()) {
+      feature.setId(innerId)
+    }
+  })
+
+  // Filter out auto-generated areas (s_auto only, not all s_*)
+  features.forEach(f => {
+    const id = f.getProperties().id
+    // 只过滤 s_auto,不过滤其他 s_* 开头的多边形
+    if (id === "s_auto") {
+      const idx = features.indexOf(f)
+      if (idx > -1) features.splice(idx, 1)
+    }
+  })
+
+  // Process bezier curves
+  for (const feature of features) {
+    if (feature.getGeometry().getType() === "MultiPoint" && feature.getProperties().id?.startsWith("b_")) {
+      const controlPoints = feature.getGeometry().getCoordinates()
+      feature.set('bezierControlPoints', controlPoints)
+      const newBezierPoints = genBezierPointsByControlPoints(controlPoints, 0.01)
+      feature.setGeometry(new LineString(newBezierPoints))
+    }
+  }
+
+  // Fix LineString with intermediate points
+  let fixedLineCount = 0
+  for (const feature of features) {
+    if (feature.getGeometry().getType() === "LineString" && feature.getId()?.startsWith("l_")) {
+      const coords = feature.getGeometry().getCoordinates()
+      if (coords.length > 2) {
+        console.warn(`检测到线段 ${feature.getId()} 包含 ${coords.length} 个点,正在修复为只包含起点和终点`)
+        feature.getGeometry().setCoordinates([coords[0], coords[coords.length - 1]])
+        fixedLineCount++
+        console.log(`线段 ${feature.getId()} 已修复: ${coords.length} 个点 -> 2 个点`)
+      }
+    }
+  }
+
+  if (fixedLineCount > 0) {
+    console.warn(`共修复了 ${fixedLineCount} 条包含中间点的线段`)
+    console.warn(`提示: 这些线段已自动修复,请保存地图以更新服务器数据`)
+  } else {
+    console.log('所有 LineString 格式正确,无需修复')
+  }
+
+  roadmap_src.value.addFeatures(features)
+
+  if (roadmapLayer.value) {
+    roadmapLayer.value.setVisible(props.showRoadNetwork)
+  }
+
+  elementRoadInitEnd(features)
+}
+
+// Complete drawing for pose init
+function completeDrawing(start, end, nid) {
+  const rotation = Math.atan2(end[0] - start[0], end[1] - start[1])
+  const yaw = Math.PI / 2.0 - rotation
+
+  if (previewFeature.value) {
+    roadmap_src.value.removeFeature(previewFeature.value)
+    previewFeature.value = null
+  }
+
+  emit('initNavigationResult', start, yaw, nid)
+}
+
+// 初始化机器人图标
+function initRobot() {
+  const robot_feature = new Feature({
+    geometry: new Point([props.robotPoseData?.x || 0, props.robotPoseData?.y || 0])
+  })
+  robot_feature.setStyle(new Style({
+    image: new Icon({
+      src: icon.value.robot,
+      rotateWithView: true,
+      rotation: 0
+    })
+  }))
+  robot_feature.setId("robot")
+
+  robot_src.value = new VectorSource({})
+  robot_src.value.addFeature(robot_feature)
+
+  map.value.addLayer(new VectorLayer({
+    source: robot_src.value,
+    zIndex: 100
+  }))
+}
+
+// 更新机器人位姿
+function robotPoseUpdateCallback(x, y, yaw) {
+  try {
+    const robot_feature = robot_src.value?.getFeatureById("robot")
+    if (!robot_feature) return
+
+    robot_feature.getStyle().getImage().setRotation(Math.PI - yaw)
+    robot_feature.getGeometry().setCoordinates([x, y])
+
+    if (props.isRobotFollow) {
+      followRotation()
+    }
+  } catch (e) {
+    console.error('更新机器人位姿失败:', e)
+  }
+}
+
+// 跟随机器人旋转
+function followRotation() {
+  const robot_feature = robot_src.value?.getFeatureById("robot")
+  if (!robot_feature || !map.value) return
+
+  const direction = robot_feature.getStyle().getImage().getRotation()
+  const center = robot_feature.getGeometry().getCoordinates()
+  map.value.getView().setCenter(center)
+  map.value.getView().animate({
+    rotation: -direction,
+    duration: 1000,
+    easing: inAndOut
+  })
+}
+
+// ==================== 点位样式 ====================
+function customizedStyle(feature) {
+  const type = feature.getGeometry().getType()
+  switch (type) {
+    case "Point":
+      return styleForPoint(feature)
+    case "LineString":
+      return styleForLine(feature)
+    case "Polygon":
+      return styleForPolygon(feature)
+    default:
+      return null
+  }
+}
+
+function styleForPoint(feature) {
+  let id = feature.values_.id
+  let point = pointAll.value.find(item => item.point == id?.substring(2))
+
+  if (props.existPointShow && id?.startsWith("p_") && pointAll.value.length > 0 && !point) {
+    return
+  }
+
+  let textInfo = ""
+  if (props.pointSwitch && point) {
+    textInfo = feature.getProperties().id?.slice(2) + "-" + point.pointName
+  } else if (props.pointSwitch) {
+    textInfo = feature.getProperties().id?.slice(2)
+  }
+
+  return [
+    new Style({
+      image: new Icon({
+        src: icon.value.point,
+        anchor: [0.5, 0.5]
+      }),
+      text: new Text({
+        font: 'normal 14px 黑体',
+        textAlign: 'center',
+        textBaseline: 'middle',
+        offsetY: -15,
+        offsetX: 0,
+        fill: new Fill({ color: 'rgb(12, 239, 23, 0.8)' }),
+        text: [textInfo, 'bold 14px sans-serif'],
+        backgroundFill: new Fill({
+          color: props.pointSwitch ? 'rgb(255,101,71, 1.0)' : 'rgb(255,101,71, 0.0)'
+        })
+      })
+    })
+  ]
+}
+
+function styleForLine(feature) {
+  const direct = feature.getProperties().direct - 100
+  const s2e = (direct & 12) > 0
+  const e2s = (direct & 3) > 0
+
+  // Arrow icons as SVG data URIs
+  const dir_forward = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+    <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
+      <polygon points="4,12 20,12 20,4 24,12 20,20 20,12" fill="#00BFFF"/>
+    </svg>
+  `)
+  const dir_backward = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+    <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
+      <polygon points="20,12 4,12 4,4 0,12 4,20 4,12" fill="#00BFFF"/>
+    </svg>
+  `)
+  const dir_two_way = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+    <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
+      <polygon points="4,12 20,12 20,4 24,12 20,20 20,12" fill="#00BFFF"/>
+      <polygon points="20,12 4,12 4,4 0,12 4,20 4,12" fill="#00BFFF"/>
+    </svg>
+  `)
+
+  let img = dir_two_way
+  if (s2e && !e2s) img = dir_forward
+  else if (!s2e && e2s) img = dir_backward
+
+  const coords = feature.getGeometry().getCoordinates()
+  const startIdx = Math.floor(coords.length / 2) - 1
+  const endIdx = Math.ceil(coords.length / 2)
+  const start = coords[startIdx]
+  const end = coords[endIdx]
+
+  const dx = end[0] - start[0]
+  const dy = end[1] - start[1]
+  const rotation = Math.atan2(dy, dx)
+
+  return [
+    new Style({
+      stroke: new Stroke({ color: "#00BFFF", width: 6 })
+    }),
+    new Style({
+      geometry: new Point([end[0] - dx / 2, end[1] - dy / 2]),
+      image: new Icon({
+        src: img,
+        anchor: [0.75, 0.5],
+        rotateWithView: true,
+        rotation: -rotation
+      })
+    })
+  ]
+}
+
+function styleForPolygon(feature) {
+  let color = feature.getProperties().color || '#7EFFFA'
+  let transparency = feature.getProperties().transparent
+  transparency = (transparency >= 0 && transparency <= 255) ? transparency : 128
+  const transparentColor = hexToRGBA(color, transparency)
+
+  return [
+    new Style({
+      fill: new Fill({ color: transparentColor }),
+      text: new Text({
+        text: feature.getProperties().name || '',
+        font: '16px serif',
+        fill: new Fill({ color: '#000' }),
+        stroke: new Stroke({ color: '#fff', width: 3 })
+      })
+    })
+  ]
+}
+
+function hexToRGBA(hex, transparency) {
+  if (hex.startsWith('#')) hex = hex.slice(1)
+  if (hex.length === 3) hex = hex.split('').map(c => c + c).join('')
+
+  let r = parseInt(hex.slice(0, 2), 16)
+  let g = parseInt(hex.slice(2, 4), 16)
+  let b = parseInt(hex.slice(4, 6), 16)
+
+  const alpha = transparency / 255
+  return `rgba(${r}, ${g}, ${b}, ${alpha})`
+}
+
+// ==================== HTML图标 ====================
+function addHtmlIcon(id, x, y, iconSrc, type = "") {
+  const iconDiv = document.createElement("div")
+  iconDiv.className = "map-icon"
+  iconDiv.style.width = "22px"
+  iconDiv.style.height = "22px"
+  iconDiv.style.borderRadius = "50%"
+  iconDiv.style.border = "2px solid #ffffff"
+  iconDiv.style.backgroundColor = "#4096BD"
+  iconDiv.style.backgroundSize = "cover"
+  iconDiv.style.position = "relative"
+  iconDiv.style.overflow = "hidden"
+
+  const numberSpan = document.createElement("span")
+  numberSpan.textContent = id
+  numberSpan.style.color = "#ffffff"
+  numberSpan.style.fontSize = "14px"
+  numberSpan.style.fontWeight = "bold"
+  numberSpan.style.position = "absolute"
+  numberSpan.style.top = "50%"
+  numberSpan.style.left = "50%"
+  numberSpan.style.transform = "translate(-50%, -50%)"
+  numberSpan.style.lineHeight = "22px"
+  numberSpan.style.whiteSpace = "nowrap"
+  iconDiv.appendChild(numberSpan)
+
+  initIconHtml(iconDiv, x, y, id, type)
+}
+
+function initIconHtml(div, x, y, id, type) {
+  const overlay = new Overlay({
+    element: div,
+    position: [x, y],
+    positioning: "center-center",
+    stopEvent: false
+  })
+  overlay.set("id", `calibration-${id}`)
+  overlay.set("type", type)
+  map.value.addOverlay(overlay)
+}
+
+function removeCalibrationById(id) {
+  calibrationList.value = calibrationList.value.filter(item => item.id !== id)
+  calibrationList.value.forEach((item, idx) => { item.id = idx + 1 })
+  refreshCalibrationOverlays()
+}
+
+function refreshCalibrationOverlays() {
+  const all = map.value.getOverlays().getArray().slice()
+
+  const toRemove = all.filter(ov => {
+    try {
+      return ov && typeof ov.get === 'function' &&
+        (ov.get('type') === 'calibration' ||
+         (typeof ov.get('id') === 'string' && ov.get('id').startsWith('calibration-')))
+    } catch (e) {
+      return false
+    }
+  })
+
+  toRemove.forEach(ov => map.value.removeOverlay(ov))
+
+  calibrationList.value.forEach(item => {
+    addHtmlIcon(item.id, item.x, item.y, '', 'calibration')
+  })
+}
+
+function removeIconHtmlById(id) {
+  const overlays = map.value.getOverlays()
+  overlays.forEach((overlay) => {
+    if (overlay && overlay.get && overlay.get("id") === id) {
+      map.value.removeOverlay(overlay)
+    }
+  })
+}
+
+// ==================== 绘制工具 ====================
+function drawPoint() {
+  clearDraw()
+  draw.value = new Draw({ source: roadmap_src.value, type: 'Point' })
+  snap.value = new Snap({ source: roadmap_src.value, pixelTolerance: 15 })
+  map.value.addInteraction(draw.value)
+  map.value.addInteraction(snap.value)
+
+  draw.value.on('drawend', (event) => {
+    const feature = event.feature
+    const featureId = 'p_' + (maxPointIdNum.value + 1)
+    feature.setId(featureId)
+    feature.set('id', featureId)
+    console.log('绘制的点 ID:', featureId)
+    console.log('绘制的点坐标:', feature.getGeometry().getCoordinates())
+    roadmap_src.value.addFeature(feature)
+    feature.setStyle(customizedStyle(feature))
+    elementRoadDrawEnd(feature, 'p')
+    map.value.removeInteraction(draw.value)
+    drawPoint()
+  })
+}
+
+function drawLine() {
+  console.log("zeinima");
+  
+  clearDraw()
+  draw.value = new Draw({ source: roadmap_src.value, type: 'LineString', maxPoints: 2 })
+  snap.value = new Snap({ source: roadmap_src.value, pixelTolerance: 10 })
+  map.value.addInteraction(draw.value)
+  map.value.addInteraction(snap.value)
+
+  draw.value.on('drawend', (event) => {
+    const feature = event.feature
+    const coordinates = feature.getGeometry().getCoordinates()
+    console.log('绘制的线坐标:', coordinates)
+    const featureId = 'l_' + (maxLineIdNum.value + 1)
+    feature.setId(featureId)
+    feature.set('id', featureId)
+
+    const startCoord = ensureValidCoordinate([...coordinates[0]])
+    const endCoord = ensureValidCoordinate([...coordinates[coordinates.length - 1]])
+
+    let startPointId = getConnectedPointId(startCoord)
+    if (!startPointId) startPointId = createPointAtCoordinate(startCoord)
+    let endPointId = getConnectedPointId(endCoord)
+    if (!endPointId) endPointId = createPointAtCoordinate(endCoord)
+
+    if (startPointId === endPointId) {
+      map.value.removeInteraction(draw.value)
+      drawLine()
+      return
+    }
+
+    // 检查是否已存在相同起点终点的线段
+    let lineExist = false
+    const startFeature = roadmap_src.value.getFeatureById(startPointId)
+    const endFeature = roadmap_src.value.getFeatureById(endPointId)
+    const featuresAtStart = roadmap_src.value.getFeaturesAtCoordinate(startFeature.getGeometry().getCoordinates())
+    for (let i = 0; i < featuresAtStart.length; i++) {
+      const existingFeature = featuresAtStart[i]
+      if (existingFeature.getGeometry().getType() !== 'LineString') continue
+
+      const props = existingFeature.getProperties()
+      if (((props.startid === startPointId) && (props.endid === endPointId)) ||
+          ((props.endid === startPointId) && (props.startid === endPointId))) {
+        console.warn('已存在相同起点终点的线段,不创建新线段')
+        lineExist = true
+        break
+      }
+    }
+
+    if (lineExist) {
+      map.value.removeInteraction(draw.value)
+      drawLine()
+      return
+    }
+
+    feature.getGeometry().setCoordinates([
+      startFeature.getGeometry().getCoordinates(),
+      endFeature.getGeometry().getCoordinates()
+    ])
+    feature.set('startid', startPointId)
+    feature.set('endid', endPointId)
+
+    roadmap_src.value.addFeature(feature)
+    feature.setStyle(customizedStyle(feature))
+    elementRoadDrawEnd(feature, 'l')
+
+    map.value.removeInteraction(draw.value)
+    drawLine()
+  })
+}
+
+function drawCurve() {
+  clearDraw()
+  draw.value = new Draw({ source: roadmap_src.value, type: 'LineString' })
+  snap.value = new Snap({ source: roadmap_src.value, pixelTolerance: 15 })
+  map.value.addInteraction(draw.value)
+  map.value.addInteraction(snap.value)
+
+  draw.value.on('drawend', (event) => {
+    const feature = event.feature
+    const coordinates = feature.getGeometry().getCoordinates()
+
+    if (coordinates.length < 3) {
+      console.warn('贝塞尔曲线需要至少3个点')
+      return
+    }
+
+    const startCoord = ensureValidCoordinate([...coordinates[0]])
+    const endCoord = ensureValidCoordinate([...coordinates[coordinates.length - 1]])
+
+    let startPointId = getConnectedPointId(startCoord)
+    let endPointId = getConnectedPointId(endCoord)
+    if (!startPointId) startPointId = createPointAtCoordinate(startCoord)
+    if (!endPointId) endPointId = createPointAtCoordinate(endCoord)
+
+    if (startPointId === endPointId) return
+
+    const startFeature = roadmap_src.value.getFeatureById(startPointId)
+    const endFeature = roadmap_src.value.getFeatureById(endPointId)
+
+    const controlPoints = [...coordinates]
+    controlPoints[0] = startFeature.getGeometry().getCoordinates()
+    controlPoints[controlPoints.length - 1] = endFeature.getGeometry().getCoordinates()
+
+    feature.set('startid', startPointId)
+    feature.set('endid', endPointId)
+    feature.set('bezierControlPoints', controlPoints)
+
+    const newBezierPoints = genBezierPointsByControlPoints(controlPoints, 0.01)
+    feature.setGeometry(new LineString(newBezierPoints))
+
+    const featureId = 'b_' + (maxBowNum.value + 1)
+    feature.setId(featureId)
+    feature.set('id', featureId)
+
+    roadmap_src.value.addFeature(feature)
+    feature.setStyle(customizedStyle(feature))
+    elementRoadDrawEnd(feature, 'b')
+
+    map.value.removeInteraction(draw.value)
+    drawCurve()
+  })
+}
+
+function drawPolygon() {
+  clearDraw()
+  draw.value = new Draw({ source: roadmap_src.value, type: 'Polygon' })
+  snap.value = new Snap({ source: roadmap_src.value, pixelTolerance: 15 })
+  map.value.addInteraction(draw.value)
+  map.value.addInteraction(snap.value)
+
+  draw.value.on('drawend', (event) => {
+    const feature = event.feature
+    const featureId = 's_' + (maxPolygonNum.value + 1)
+    feature.setId(featureId)
+    feature.set('id', featureId)
+    console.log('绘制的面 ID:', featureId)
+    console.log('绘制的面坐标:', feature.getGeometry().getCoordinates())
+    roadmap_src.value.addFeature(feature)
+    feature.setStyle(customizedStyle(feature))
+    elementRoadDrawEnd(feature, 's')
+    map.value.removeInteraction(draw.value)
+    drawPolygon()
+  })
+}
+
+function clearDraw() {
+  if (draw.value) {
+    map.value.removeInteraction(draw.value)
+    draw.value = null
+  }
+  if (snap.value) {
+    map.value.removeInteraction(snap.value)
+    snap.value = null
+  }
+}
+
+function createPointAtCoordinate(targetCoord, data = []) {
+  const features = roadmap_src.value.getFeatures()
+  for (const feature of features) {
+    const geometry = feature.getGeometry()
+    if (geometry && geometry.getType() === 'Point') {
+      const coord = geometry.getCoordinates()
+      if (coord[0] === targetCoord[0] && coord[1] === targetCoord[1]) {
+        ElMessage({ message: '目标绘制点已存在点位', type: 'warning' })
+        console.log('目标绘制点已存在点位')
+        return null
+      }
+    }
+  }
+
+  const pointFeature = new Feature({ geometry: new Point(targetCoord) })
+  const pointId = 'p_' + (maxPointIdNum.value + 1)
+  pointFeature.set('id', pointId)
+  pointFeature.setId(pointId)
+
+  if (Array.isArray(data) && data.length > 0) {
+    data.forEach(item => {
+      Object.entries(item).forEach(([key, value]) => {
+        pointFeature.set(key, value)
+      })
+    })
+  }
+
+  roadmap_src.value.addFeature(pointFeature)
+  pointFeature.setStyle(customizedStyle(pointFeature))
+  elementRoadDrawEnd(pointFeature, 'p')
+  console.log(`成功创建点位: ${pointId}`, targetCoord)
+
+  return pointId
+}
+
+function getConnectedPointId(coord) {
+  const tolerance = 0.01
+  let closestPointId = ''
+  roadmap_src.value.forEachFeature((feature) => {
+    if (feature.getGeometry().getType() === 'Point') {
+      const pointCoord = feature.getGeometry().getCoordinates()
+      if (isCoordsClose(pointCoord, coord, tolerance)) {
+        closestPointId = feature.getId()
+      }
+    }
+  })
+  return closestPointId
+}
+
+function isCoordsClose(coord1, coord2, tolerance) {
+  const dx = Math.abs(coord1[0] - coord2[0])
+  const dy = Math.abs(coord1[1] - coord2[1])
+  return dx <= tolerance && dy <= tolerance
+}
+
+function ensureValidCoordinate(coord) {
+  if (!coord || coord.length < 2) return coord
+  if (coord[0] === null || coord[0] === undefined) coord[0] = 0
+  if (coord[1] === null || coord[1] === undefined) coord[1] = 0
+  if (coord.length < 3) coord.push(0)
+  else if (coord[2] === null || coord[2] === undefined) coord[2] = 0
+  return coord
+}
+
+// 贝塞尔曲线
+function genBezierPointsByControlPoints(controlPoints, step = 0.05) {
+  const bezierPoints = []
+  const n = controlPoints.length - 1
+  for (let i = 0; i < 1; i += step) {
+    let x = 0, y = 0
+    for (let idx = 0; idx < controlPoints.length; idx++) {
+      const coord = controlPoints[idx]
+      if (!idx) {
+        x += coord[0] * Math.pow(1 - i, n - idx) * Math.pow(i, idx)
+        y += coord[1] * Math.pow(1 - i, n - idx) * Math.pow(i, idx)
+      } else {
+        x += (factorial(n) / factorial(idx) / factorial(n - idx)) * coord[0] * Math.pow(1 - i, n - idx) * Math.pow(i, idx)
+        y += (factorial(n) / factorial(idx) / factorial(n - idx)) * coord[1] * Math.pow(1 - i, n - idx) * Math.pow(i, idx)
+      }
+    }
+    bezierPoints.push([x, y])
+  }
+  bezierPoints.push(controlPoints[n])
+  return bezierPoints
+}
+
+function factorial(num) {
+  return num <= 1 ? 1 : num * factorial(num - 1)
+}
+
+// 模式切换逻辑
+function togglePointSelection() {
+  // This would toggle a ref, but since it's a prop we emit an event
+  emit('togglePointSelection')
+}
+
+// 计算航向角
+function calculateBearing(start, end) {
+  const dx = end[0] - start[0]
+  const dy = end[1] - start[1]
+  const angle = Math.atan2(dy, dx) * (180 / Math.PI)
+  return angle < 0 ? angle + 360 : angle
+}
+
+// 修改多边形的颜色和透明度
+function editSnapeColor(id) {
+  if (!roadmap_src.value) {
+    console.log("请先初始化画布!")
+    return
+  }
+  const snapeFeature = roadmap_src.value.getFeatureById(id)
+  if (!snapeFeature) {
+    console.log(`未找到ID为 ${id} 的多边形元素!`)
+    return
+  }
+  snapeFeature.setStyle(customizedStyle(snapeFeature))
+}
+
+// ==================== 元素操作 ====================
+function elementRoadInitEnd(features) {
+  let data = features.reduce((acc, item) => {
+    const id = item.values_.id || item.getId()
+    if (!id) return acc
+    const [prefix, number] = id.split("_")
+    const num = parseInt(number, 10)
+    if (!acc[prefix] || acc[prefix] < num) {
+      acc[prefix] = num
+    }
+    return acc
+  }, {})
+
+  if (data.p) maxPointIdNum.value = data.p
+  if (data.l) maxLineIdNum.value = data.l
+  if (data.b) maxBowNum.value = data.b
+  if (data.s) maxPolygonNum.value = data.s
+
+  emit('elementRoadInitEnd', features)
+}
+
+function elementRoadDrawEnd(feature, type) {
+  const typeMapping = { p: 'maxPointIdNum', l: 'maxLineIdNum', b: 'maxBowNum', s: 'maxPolygonNum' }
+  if (typeMapping[type]) {
+    switch (type) {
+      case 'p': maxPointIdNum.value++; break
+      case 'l': maxLineIdNum.value++; break
+      case 'b': maxBowNum.value++; break
+      case 's': maxPolygonNum.value++; break
+    }
+  }
+  emit('elementRoadDrawEnd', feature)
+}
+
+function removeElement(id) {
+  if (!roadmap_src.value) {
+    emit('removeElementResult', '')
+    return
+  }
+  const feature = roadmap_src.value.getFeatureById(id)
+  if (feature) {
+    roadmap_src.value.removeFeature(feature)
+    emit('removeElementResult', id)
+    if (selectedFeatureId.value === id) {
+      selectedFeatureId.value = ''
+    }
+  } else {
+    emit('removeElementResult', '')
+  }
+}
+
+function selectShowEle(id) {
+  if (!id || !roadmap_src.value) return
+
+  let feature = roadmap_src.value.getFeatureById(id)
+  if (!feature) return
+
+  let position = feature.getGeometry().getCoordinates()
+  if (id.startsWith('l')) {
+    position = calculateCenterCoordinate(position[0], position[1], 0.4)
+  } else if (id.startsWith('b')) {
+    const coords = position
+    position = coords[Math.ceil(coords.length / 2)]
+  } else if (id.startsWith('s')) {
+    position = calculateCentroid(position)
+  }
+
+  // Centering the map view on the selected element
+  if (map.value) {
+    const view = map.value.getView()
+    const currentZoom = view.getZoom()
+    const targetZoom = Math.max(currentZoom, 15)
+    view.animate({
+      center: position,
+      zoom: targetZoom,
+      duration: 500
+    })
+  }
+
+  if (!icon_src.value) {
+    // For points, show icon at exact position (not offset)
+    // For other elements, show icon at calculated position
+    const iconX = id.startsWith('p') ? position[0] : position[0]
+    const iconY = id.startsWith('p') ? position[1] : position[1] + 0.8
+    initIcon(1, iconX, iconY, icon.value.mark, 'selectEle', 0.15, 100)
+    selectedFeatureId.value = id
+  } else {
+    let oldFeature = icon_src.value.getFeatureById('selectEle-' + 1)
+    if (!oldFeature) {
+      const iconX = id.startsWith('p') ? position[0] : position[0]
+      const iconY = id.startsWith('p') ? position[1] : position[1] + 0.8
+      initIcon(1, iconX, iconY, icon.value.mark, 'selectEle', 0.15, 100)
+      selectedFeatureId.value = id
+    } else {
+      if (id === selectedFeatureId.value) {
+        // Toggle off - don't remove, just update position
+        icon_src.value.removeFeature(oldFeature)
+        selectedFeatureId.value = ''
+      } else {
+        // For points, show icon at exact position
+        const iconX = id.startsWith('p') ? position[0] : position[0]
+        const iconY = id.startsWith('p') ? position[1] : position[1] + 0.8
+        oldFeature.getGeometry().setCoordinates([iconX, iconY])
+        selectedFeatureId.value = id
+      }
+    }
+  }
+
+  emit('selectShowEleResult', feature)
+}
+
+function initIcon(id, x, y, iconSrc, type = "", scale = 0.15, zIndex) {
+  const icon_feature = new Feature({ geometry: new Point([x, y]) })
+  icon_feature.setStyle(new Style({
+    image: new Icon({ src: iconSrc, rotateWithView: true, rotation: 0, scale })
+  }))
+  icon_feature.setId(type + "-" + id)
+
+  icon_src.value = new VectorSource({})
+  icon_src.value.addFeature(icon_feature)
+
+  const icon_layer = new VectorLayer({ source: icon_src.value })
+  if (zIndex !== undefined) icon_layer.setZIndex(zIndex)
+  map.value.addLayer(icon_layer)
+}
+
+function calculateCenterCoordinate(coord1, coord2, ratio = 0.5) {
+  const x1 = coord1[0], y1 = coord1[1]
+  const x2 = coord2[0], y2 = coord2[1]
+  return [x1 + ratio * (x2 - x1), y1 + ratio * (y2 - y1)]
+}
+
+function calculateCentroid(coords) {
+  if (Array.isArray(coords[0]) && Array.isArray(coords[0][0])) {
+    coords = coords[0]
+  }
+  let n = coords.length
+  let area = 0, cx = 0, cy = 0
+  for (let i = 0; i < n; i++) {
+    let j = (i + 1) % n
+    let x1 = coords[i][0], y1 = coords[i][1]
+    let x2 = coords[j][0], y2 = coords[j][1]
+    let crossProduct = x1 * y2 - x2 * y1
+    area += crossProduct
+    cx += (x1 + x2) * crossProduct
+    cy += (y1 + y2) * crossProduct
+  }
+  area = area / 2
+  cx = cx / (6 * area)
+  cy = cy / (6 * area)
+  return [cx, cy]
+}
+
+function editElementSelectHandle(feature) {
+  if (feature) {
+    selectShowEle(feature.getId())
+  }
+}
+
+function hideToop() {
+  const tooltip = document.getElementById('tooltip')
+  if (tooltip) tooltip.style.display = 'none'
+}
+
+function restIdNum() {
+  maxIndex.value = 0
+}
+
+// ==================== 轨迹相关 ====================
+function initTrajectoryLayer() {
+  if (!trajectorySource.value) {
+    trajectorySource.value = new VectorSource()
+    map.value.addLayer(new VectorLayer({
+      source: trajectorySource.value,
+      zIndex: 5
+    }))
+  }
+}
+
+function drawTrajectory(trajectory, options = {}) {
+  console.log("开始绘制轨迹:", trajectory, options)
+
+  if (!trajectory || trajectory.length === 0) {
+    console.warn("轨迹数据为空")
+    return
+  }
+
+  initTrajectoryLayer()
+  clearTrajectory()
+  currentTrajectory.value = trajectory
+  trajectoryProgress.value = options.currentProgress || 0
+
+  const robotPos = options.robotPosition || [0, 0]
+  const projectionInfo = findRobotProjectionOnTrajectory(trajectory, robotPos)
+
+  if (projectionInfo && projectionInfo.segmentIndex >= 0) {
+    drawRealtimeTrajectoryFromRobotPosition(trajectory, robotPos, projectionInfo)
+  } else {
+    drawTrajectoryByProgress(trajectory)
+  }
+
+  if (trajectory.length > 0) {
+    addTrajectoryMarker(trajectory[0], { text: '起点', color: '#ffffff', backgroundColor: 'rgba(0, 255, 0, 0.8)' })
+  }
+  if (trajectory.length > 1) {
+    addTrajectoryMarker(trajectory[trajectory.length - 1], { text: '终点', color: '#ffffff', backgroundColor: 'rgba(255, 0, 0, 0.8)' })
+  }
+
+  console.log("轨迹绘制完成,总点数:", trajectory.length, "当前进度:", trajectoryProgress.value)
+}
+
+function findRobotProjectionOnTrajectory(trajectory, robotPos) {
+  if (!trajectory || trajectory.length < 2) return null
+
+  let minDistance = Infinity, bestProjection = null
+
+  for (let i = 0; i < trajectory.length - 1; i++) {
+    const projection = projectPointToLineSegment(robotPos, trajectory[i], trajectory[i + 1])
+    if (projection.distance < minDistance) {
+      minDistance = projection.distance
+      bestProjection = {
+        point: projection.point,
+        distance: projection.distance,
+        segmentIndex: i,
+        parameter: getParameterOnSegment(projection.point, trajectory[i], trajectory[i + 1])
+      }
+    }
+  }
+
+  // 放宽距离限制,提高轨迹绘制的实时性
+  if (bestProjection && bestProjection.distance < 10.0) {
+    console.log(`找到机器人投影点: 线段${bestProjection.segmentIndex}, 距离${bestProjection.distance.toFixed(2)}m`)
+    return bestProjection
+  }
+
+  console.log(`机器人距离轨迹太远: ${bestProjection ? bestProjection.distance.toFixed(2) : 'N/A'}m, 使用回退绘制方式`)
+  return null
+}
+
+function projectPointToLineSegment(point, lineStart, lineEnd) {
+  const [px, py] = point
+  const [x1, y1] = lineStart
+  const [x2, y2] = lineEnd
+  const A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1
+  const dot = A * C + B * D
+  const lenSq = C * C + D * D
+  if (lenSq === 0) return { point: [x1, y1], distance: Math.sqrt(A * A + B * B) }
+
+  let param = Math.max(0, Math.min(1, dot / lenSq))
+  return {
+    point: [x1 + param * C, y1 + param * D],
+    distance: Math.sqrt(Math.pow(px - (x1 + param * C), 2) + Math.pow(py - (y1 + param * D), 2))
+  }
+}
+
+// 获取投影点在线段上的参数(0-1)
+function getParameterOnSegment(projectionPoint, segmentStart, segmentEnd) {
+  const [px, py] = projectionPoint
+  const [x1, y1] = segmentStart
+  const [x2, y2] = segmentEnd
+
+  if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) {
+    return (px - x1) / (x2 - x1)
+  } else {
+    return (py - y1) / (y2 - y1)
+  }
+}
+
+function drawRealtimeTrajectoryFromRobotPosition(trajectory, robotPos, projectionInfo) {
+  console.log(`实时轨迹绘制: 机器人位置(${robotPos[0].toFixed(2)}, ${robotPos[1].toFixed(2)}), 投影在线段${projectionInfo.segmentIndex}`)
+
+  const completedPoints = []
+  for (let i = 0; i <= projectionInfo.segmentIndex; i++) completedPoints.push(trajectory[i])
+  completedPoints.push(projectionInfo.point)
+
+  if (completedPoints.length >= 2) {
+    console.log(`绘制实时已走过轨迹: 起点到机器人投影位置, ${completedPoints.length}个点`)
+    drawTrajectorySegment(completedPoints, { color: '#00ff00', width: 4, lineDash: [10, 5], type: 'completed' })
+  }
+
+  const remainingPoints = [projectionInfo.point]
+  if (projectionInfo.segmentIndex < trajectory.length - 1) remainingPoints.push(trajectory[projectionInfo.segmentIndex + 1])
+  for (let i = projectionInfo.segmentIndex + 2; i < trajectory.length; i++) remainingPoints.push(trajectory[i])
+
+  if (remainingPoints.length >= 2) {
+    console.log(`绘制实时未走过轨迹: 机器人投影位置到终点, ${remainingPoints.length}个点`)
+    drawTrajectorySegment(remainingPoints, { color: '#ff0000', width: 4, lineDash: [10, 5], type: 'remaining' })
+  }
+
+  addRealtimeRobotMarker(robotPos, projectionInfo.point)
+}
+
+function drawTrajectoryByProgress(trajectory) {
+  if (trajectoryProgress.value > 0) {
+    const completed = trajectory.slice(0, trajectoryProgress.value + 1)
+    if (completed.length >= 2) drawTrajectorySegment(completed, { color: '#00ff00', width: 4, lineDash: [10, 5], type: 'completed' })
+  }
+  if (trajectoryProgress.value < trajectory.length - 1) {
+    const remaining = trajectory.slice(trajectoryProgress.value)
+    if (remaining.length >= 2) drawTrajectorySegment(remaining, { color: '#ff0000', width: 4, lineDash: [10, 5], type: 'remaining' })
+  }
+}
+
+function drawTrajectorySegment(trajectorySegment, style) {
+  if (!trajectorySegment || trajectorySegment.length < 2) return
+  const feature = new Feature({ geometry: new LineString(trajectorySegment) })
+  feature.setStyle(new Style({ stroke: new Stroke({ color: style.color, width: style.width, lineDash: style.lineDash }) }))
+  feature.setId(`trajectory-${style.type}-${Date.now()}`)
+  trajectorySource.value.addFeature(feature)
+}
+
+function addTrajectoryMarker(position, options) {
+  const marker = new Feature({ geometry: new Point(position) })
+  marker.setStyle(new Style({
+    text: new Text({
+      text: options.text, font: 'bold 12px Arial',
+      fill: new Fill({ color: options.color }),
+      backgroundFill: new Fill({ color: options.backgroundColor }),
+      padding: [3, 6, 3, 6], offsetY: -20
+    }),
+    image: new Icon({
+      src: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg"><circle cx="6" cy="6" r="5" fill="${options.color}" stroke="white" stroke-width="1"/></svg>`),
+      scale: 1
+    })
+  }))
+  marker.setId(`trajectory-marker-${options.text}-${Date.now()}`)
+  trajectorySource.value.addFeature(marker)
+}
+
+function addRealtimeRobotMarker(robotActualPos, robotProjectionPos) {
+  const marker = new Feature({ geometry: new Point(robotProjectionPos) })
+  marker.setStyle(new Style({
+    image: new Icon({
+      src: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(`
+        <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+          <circle cx="8" cy="8" r="6" fill="#ffff00" stroke="#ff6600" stroke-width="2"/>
+          <circle cx="8" cy="8" r="2" fill="#ff6600"/>
+        </svg>
+      `), scale: 1
+    }),
+    text: new Text({
+      text: '当前', font: 'bold 10px Arial',
+      fill: new Fill({ color: '#333' }),
+      backgroundFill: new Fill({ color: 'rgba(255,255,255,0.8)' }),
+      padding: [2, 4, 2, 4], offsetY: -25
+    })
+  }))
+  marker.setId(`trajectory-current-position-${Date.now()}`)
+  trajectorySource.value.addFeature(marker)
+}
+
+function clearTrajectory() {
+  if (trajectorySource.value) trajectorySource.value.clear()
+  currentTrajectory.value = null
+  trajectoryProgress.value = 0
+}
+
+function updateTrajectoryProgress(newProgress, robotPosition = null) {
+  if (!currentTrajectory.value) return
+  trajectoryProgress.value = newProgress
+  if (trajectorySource.value) trajectorySource.value.clear()
+  const robotPos = robotPosition || [props.robotPoseData?.x || 0, props.robotPoseData?.y || 0]
+  drawTrajectory(currentTrajectory.value, { currentProgress: newProgress, robotPosition: robotPos })
+}
+
+function updateRealtimeTrajectory(robotPosition) {
+  if (!currentTrajectory.value || !trajectorySource.value) return
+  const features = trajectorySource.value.getFeatures()
+  features.forEach(feature => {
+    const id = feature.getId()
+    if (id && (id.startsWith('trajectory-completed-') || id.startsWith('trajectory-remaining-') || id.startsWith('trajectory-current-position-'))) {
+      trajectorySource.value.removeFeature(feature)
+    }
+  })
+  const projectionInfo = findRobotProjectionOnTrajectory(currentTrajectory.value, robotPosition)
+  if (projectionInfo && projectionInfo.segmentIndex >= 0) {
+    drawRealtimeTrajectoryFromRobotPosition(currentTrajectory.value, robotPosition, projectionInfo)
+  }
+}
+
+// ==================== 点云相关 ====================
+function initPointcloudLayer() {
+  pointcloudLayer.value = new VectorLayer({
+    source: new VectorSource({
+      features: [new Feature({ geometry: new MultiPoint([]), name: 'PointCloud' })]
+    }),
+    style: new Style({
+      image: new Circle({ radius: 1.8, fill: new Fill({ color: '#FF0000' }) })
+    }),
+    visible: props.showPointcloud,
+    zIndex: 10
+  })
+  map.value.addLayer(pointcloudLayer.value)
+
+  if (props.showPointcloud) {
+    startPointcloudUpdate()
+  }
+}
+
+function startPointcloudUpdate() {
+  stopPointcloudUpdate()
+  pointcloudTimer.value = setInterval(() => {
+    if (!pointcloudLayer.value || !pointcloudLayer.value.getVisible()) return
+    updatePointcloud()
+  }, 800)
+}
+
+function stopPointcloudUpdate() {
+  if (pointcloudTimer.value) {
+    clearInterval(pointcloudTimer.value)
+    pointcloudTimer.value = null
+  }
+}
+
+async function updatePointcloud() {
+  try {
+    const response = await getPointcloud()
+    if (!pointcloudLayer.value || !response) return
+
+    const pointcloud = response
+    const pointsCount = Math.floor(pointcloud.byteLength / 12)
+    if (pointsCount === 0) return
+
+    const float32View = new Float32Array(pointcloud, 0)
+    const coordinates = []
+
+    for (let i = 0; i < pointsCount; i++) {
+      const x = float32View[3 * i]
+      const y = float32View[3 * i + 1]
+      coordinates.push([x, y])
+    }
+
+    const features = pointcloudLayer.value.getSource().getFeatures()
+    if (features.length > 0) {
+      features[0].setGeometry(new MultiPoint(coordinates))
+    }
+
+    emit('point-cloud-load-end', pointsCount)
+  } catch (e) {
+    // Silent fail to avoid console spam
+  }
+}
+
+// ==================== 路网导入导出 ====================
+function clearAllElements() {
+  if (!roadmap_src.value) return false
+  roadmap_src.value.clear()
+  selectedFeatureId.value = ''
+  if (icon_src.value) icon_src.value.clear()
+  return true
+}
+
+function importGeoJsonFeatures(geoJsonData) {
+  if (!roadmap_src.value) {
+    throw new Error("请先初始化画布!")
+  }
+
+  if (!geoJsonData || !geoJsonData.features) {
+    throw new Error("无效的GeoJSON数据")
+  }
+
+  const features = new GeoJSON().readFeatures(geoJsonData)
+
+  features.forEach(feature => {
+    if (!feature.getId() && feature.get('id')) {
+      feature.setId(feature.get('id'))
+    }
+  })
+
+  roadmap_src.value.addFeatures(features)
+
+  if (features.length > 0) {
+    emit('elementRoadInitEnd', features)
+  }
+
+  console.log(`成功导入${features.length}个地图元素`)
+  return features
+}
+
+function createPointAtCoordinateForImport(coordinates, name = '', extraData = []) {
+  if (!roadmap_src.value) return
+
+  ensureValidCoordinate(coordinates)
+
+  const existingPoints = roadmap_src.value.getFeatures().filter(f => f.getId() && f.getId().startsWith('p_'))
+  const maxId = existingPoints.length > 0 ? Math.max(...existingPoints.map(f => parseInt(f.getId().split('_')[1]) || 0)) : 0
+  const newId = `p_${maxId + 1}`
+
+  const pointFeature = new Feature({
+    geometry: new Point(coordinates),
+    id: newId,
+    name: name || `点位${maxId + 1}`
+  })
+  pointFeature.setId(newId)
+
+  if (extraData && extraData.length > 0) {
+    extraData.forEach(data => {
+      Object.keys(data).forEach(key => pointFeature.set(key, data[key]))
+    })
+  }
+
+  roadmap_src.value.addFeature(pointFeature)
+  emit('elementRoadDrawEnd', pointFeature)
+
+  return pointFeature
+}
+
+function refresh() {
+  if (map.value) {
+    map.value.render()
+    map.value.updateSize()
+  }
+}
+
+function updateMapSize() {
+  if (map.value) {
+    // 确保地图容器有正确的尺寸
+    const mapNode = document.getElementById("tileMap")
+    if (mapNode) {
+      // 强制触发浏览器重绘
+      mapNode.style.width = '1px'
+      mapNode.style.width = props.width || '100%'
+      mapNode.style.height = props.height || '100%'
+    }
+    map.value.updateSize()
+  }
+}
+
+function setRoadNetworkVisible(visible) {
+  if (roadmapLayer.value) {
+    roadmapLayer.value.setVisible(visible)
+  }
+}
+
+// ==================== 点位移动 ====================
+function pointPositionUpdate(id, position) {
+  if (!roadmap_src.value) return
+
+  const feature = roadmap_src.value.getFeatureById(id)
+  if (!feature) return
+
+  const geometry = feature.getGeometry()
+  const coordinates = geometry.getCoordinates()
+  coordinates[0] = position[0]
+  coordinates[1] = position[1]
+  geometry.setCoordinates(coordinates)
+
+  // Update selection icon position - for points, show at exact position (no offset)
+  if (icon_src.value) {
+    const iconFeature = icon_src.value.getFeatureById('selectEle-' + 1)
+    if (iconFeature) {
+      const iconY = id.startsWith('p') ? position[1] : position[1] + 0.8
+      iconFeature.getGeometry().setCoordinates([position[0], iconY])
+    }
+  }
+
+  // Update connected lines
+  roadmap_src.value.getFeatures().forEach(lineFeature => {
+    const startId = lineFeature.get('startid')
+    const endId = lineFeature.get('endid')
+
+    if (lineFeature.getId()?.startsWith('l')) {
+      if (startId === id || endId === id) {
+        const startFeature = roadmap_src.value.getFeatureById(startId)
+        const endFeature = roadmap_src.value.getFeatureById(endId)
+        if (startFeature && endFeature) {
+          lineFeature.getGeometry().setCoordinates([
+            startFeature.getGeometry().getCoordinates(),
+            endFeature.getGeometry().getCoordinates()
+          ])
+          lineFeature.setStyle(customizedStyle(lineFeature))
+        }
+      }
+    } else if (lineFeature.getId()?.startsWith('b')) {
+      if (startId === id || endId === id) {
+        const controlPoints = lineFeature.getGeometry().getCoordinates()
+        const updatedControlPoints = controlPoints.map(p => [...p])
+        if (startId === id) updatedControlPoints[0] = position
+        if (endId === id) updatedControlPoints[updatedControlPoints.length - 1] = position
+        const newBezierPoints = genBezierPointsByControlPoints(updatedControlPoints, 0.01)
+        lineFeature.getGeometry().setCoordinates(newBezierPoints)
+        lineFeature.setStyle(customizedStyle(lineFeature))
+      }
+    }
+  })
+}
+
+// ==================== 暴露方法 ====================
+defineExpose({
+  map,
+  roadmap_src,
+  loadMap,
+  loadRoadmap: initRoad,
+  loadPointCloud: updatePointcloud,
+  importGeoJsonFeatures,
+  removeElement,
+  setCenter: (coords) => map.value?.getView().animate({ center: coords, duration: 500 }),
+  drawPoint,
+  drawLine,
+  drawCurve,
+  drawPolygon,
+  clearDraw,
+  selectShowEle,
+  removeIconHtmlById,
+  clearAllElements,
+  clearTrajectory,
+  drawTrajectory,
+  updateTrajectoryProgress,
+  updateRealtimeTrajectory,
+  initTrajectoryLayer,
+  pointPositionUpdate,
+  createPointAtCoordinate,
+  createPointAtCoordinateForImport,
+  refresh,
+  updateMapSize,
+  setRoadNetworkVisible,
+  followRotation,
+  initRobot,
+  robotPoseUpdateCallback
+})
+</script>
+
+<style scoped>
+.map-wrapper {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.mapParent {
+  width: 100%;
+  height: 100%;
+  min-height: 300px;
+  background-color: #d5d8e1;
+  text-align: center !important;
+  border-radius: 5px;
+  position: relative;
+  box-sizing: border-box;
+}
+
+.maptext {
+  font-size: 20px;
+  color: rgb(156, 156, 156);
+  height: 100%;
+}
+
+.tooltip {
+  position: absolute;
+  background: white;
+  border-radius: 12px;
+  display: none;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
+  width: 240px;
+  overflow: hidden;
+  border: 1px solid #e2e8f0;
+  z-index: 1000;
+}
+
+.tooltip-header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  padding: 12px 16px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.tooltip-title {
+  font-size: 14px;
+  font-weight: 600;
+  margin: 0;
+}
+
+.tooltip-content {
+  padding: 16px;
+  background: #f8fafc;
+}
+
+.tooltip-content p {
+  margin: 0;
+  padding: 8px 12px;
+  background: white;
+  border-radius: 6px;
+  border: 1px solid #e2e8f0;
+  text-align: center;
+}
+
+.tooltip-content p:first-child {
+  font-size: 13px;
+  color: #4a5568;
+  margin-bottom: 8px;
+}
+
+.tooltip-content p:last-child {
+  font-family: 'Courier New', monospace;
+  color: #e53e3e;
+  font-weight: 600;
+  font-size: 14px;
+  background: #fff5f5;
+  border-color: #fed7d7;
+}
+
+.tooltip-actions {
+  padding: 12px 16px;
+  background: white;
+  display: flex;
+  gap: 8px;
+  border-top: 1px solid #e2e8f0;
+}
+
+.action-btn {
+  flex: 1;
+  border-radius: 6px;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+}
+
+:deep(.action-btn.el-button--primary) {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+}
+
+:deep(.action-btn:not(.el-button--primary)) {
+  background: #f7fafc;
+  border: 1px solid #e2e8f0;
+  color: #4a5568;
+}
+
+:deep(.tileMap) {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 738 - 0
src/composables/useMap.js

@@ -0,0 +1,738 @@
+/**
+ * 地图模块组合式函数
+ * 封装地图列表、导航、编辑等功能的API调用
+ *
+ * 接口返回格式说明:
+ * - getMapList: { status: true, maps: [], states: [] }
+ * - getCurrentMap: { status: true, name: 'xxx' }
+ * - renameMap: { status: true }
+ * - deleteMap: { status: true }
+ */
+import { ref, computed } from 'vue'
+import { ElMessage, ElLoading } from 'element-plus'
+import { saveAs } from 'file-saver'
+import * as mapApi from '@/api/robot/map'
+import * as controlApi from '@/api/robot/control'
+
+export function useMapList() {
+  const loading = ref(false)
+  const mapList = ref([])
+  const currentMap = ref(null)
+  const selectedMap = ref(null)
+  const thumbnailCache = new Map() // 缩略图缓存
+
+  // 处理后端返回的地图列表数据
+  // 后端返回格式: { status: true, maps: ['map1', 'map2'], states: ['available', 'building'] }
+  function processMapListResponse(response) {
+    if (!response) return []
+
+    // 兼容两种返回格式
+    // 格式1: { status: true, maps: [], states: [] }
+    // 格式2: { code: 200, data: [...] }
+    if (response.maps && Array.isArray(response.maps)) {
+      return response.maps.map((mapName, index) => ({
+        id: index + 1,
+        map: mapName,
+        mapName: mapName,
+        name: mapName,
+        state: response.states && response.states[index] ? response.states[index] : 'unavailable'
+      }))
+    }
+
+    // 如果是标准响应格式 { code: 200, data: [...] }
+    if (response.data && Array.isArray(response.data)) {
+      return response.data.map((item, index) => ({
+        id: item.id || index + 1,
+        map: item.map || item.mapName || item.name,
+        mapName: item.mapName || item.map || item.name,
+        name: item.name || item.map || item.mapName,
+        state: item.state || item.status || 'available',
+        thumbUrl: item.thumbUrl || null
+      }))
+    }
+
+    return []
+  }
+
+  // 获取地图列表
+  async function fetchMapList(deviceId) {
+    loading.value = true
+    try {
+      const res = await mapApi.getMapList(deviceId)
+
+      // 兼容处理:request可能已经解包了data
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || (res.code === 200)) {
+        mapList.value = processMapListResponse(responseData)
+      } else if (res.code !== 200) {
+        ElMessage.error(res.msg || '获取地图列表失败')
+        mapList.value = []
+      }
+
+      return res
+    } catch (error) {
+      console.error('获取地图列表失败:', error)
+      ElMessage.error('获取地图列表失败')
+      mapList.value = []
+      throw error
+    } finally {
+      loading.value = false
+    }
+  }
+
+  // 获取当前地图
+  async function fetchCurrentMap(deviceId) {
+    try {
+      const res = await mapApi.getCurrentMap(deviceId)
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || (res.code === 200)) {
+        currentMap.value = responseData.name || responseData.data
+      }
+      return res
+    } catch (error) {
+      console.error('获取当前地图失败:', error)
+      throw error
+    }
+  }
+
+  // 获取地图缩略图
+  async function fetchMapThumbnail(mapName) {
+    if (!mapName) return null
+
+    // 检查缓存
+    if (thumbnailCache.has(mapName)) {
+      return thumbnailCache.get(mapName)
+    }
+
+    try {
+      const res = await mapApi.getMapThumbnail(mapName)
+      
+      // 检查是否是有效的blob
+      // if (blob instanceof Blob && blob.size > 0) {
+      //   const url = URL.createObjectURL(blob)
+      //   thumbnailCache.set(mapName, url)
+      //   return url
+      // }
+      if(res.code === 200 && res.data) {
+        return res.data
+      }
+        
+      return null
+    } catch (error) {
+      console.error('获取缩略图失败:', error)
+      return null
+    }
+  }
+
+  // 批量获取缩略图
+  async function fetchThumbnailsForList(mapItems, limit = 6) {
+    const items = mapItems.slice(0, limit)
+
+    for (const item of items) {
+      const mapName = item.map || item.mapName || item.name
+      if (mapName && !item.thumbUrl) {
+        try {
+          const thumbUrl = await fetchMapThumbnail(mapName)
+          if (thumbUrl) {
+            item.thumbUrl = thumbUrl
+          }
+        } catch (e) {
+          // 忽略单个缩略图加载失败
+        }
+      }
+    }
+
+    return mapItems
+  }
+
+  // 清除缩略图缓存
+  function clearThumbnailCache() {
+    thumbnailCache.forEach(url => URL.revokeObjectURL(url))
+    thumbnailCache.clear()
+  }
+
+  // 重命名地图
+  async function renameMap(oldName, newName) {
+    try {
+      const res = await mapApi.renameMap({ map: oldName, rename: newName })
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || res.code === 200) {
+        ElMessage.success('重命名成功')
+        await fetchMapList()
+      } else {
+        ElMessage.error(responseData.msg || res.msg || '重命名失败')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('重命名失败')
+      throw error
+    }
+  }
+
+  // 删除地图
+  async function removeMap(mapName) {
+    try {
+      const res = await mapApi.deleteMap({ map: mapName })
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || res.code === 200) {
+        ElMessage.success('删除成功')
+        mapList.value = mapList.value.filter(m => (m.map || m.mapName || m.name) !== mapName)
+      } else {
+        ElMessage.error(responseData.msg || res.msg || '删除失败')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('删除失败')
+      throw error
+    }
+  }
+
+  // 下载地图
+  async function downloadMap(mapName, components) {
+    const loading = ElLoading.service({
+      text: '正在压缩地图文件,请耐心等待...',
+      background: 'rgba(0, 0, 0, 0.7)'
+    })
+
+    try {
+      // 1. 压缩
+      await mapApi.compressMapExport({ map: mapName, ignore: [] })
+
+      // 2. 下载
+      const blob = await mapApi.downloadMapExport(mapName)
+      if (blob instanceof Blob && blob.size > 0) {
+        saveAs(blob, `${mapName}.zip`)
+        ElMessage.success('下载成功')
+      } else {
+        ElMessage.error('下载失败:文件无效')
+      }
+    } catch (error) {
+      if (error.message?.includes('timeout')) {
+        ElMessage.error('下载超时,请稍后重试')
+      } else {
+        ElMessage.error('下载失败')
+      }
+    } finally {
+      loading.close()
+    }
+  }
+
+  // 选择地图
+  function selectMap(map) {
+    selectedMap.value = map
+  }
+
+  return {
+    loading,
+    mapList,
+    currentMap,
+    selectedMap,
+    fetchMapList,
+    fetchCurrentMap,
+    fetchMapThumbnail,
+    fetchThumbnailsForList,
+    clearThumbnailCache,
+    renameMap,
+    removeMap,
+    downloadMap,
+    selectMap
+  }
+}
+
+export function useMapControl(deviceId) {
+  // 录制/构建/SLAM状态
+  const isRecording = ref(false)
+  const isBuilding = ref(false)
+  const isSlaming = ref(false)
+  const recordingMapName = ref('')
+  const buildingMapName = ref('')
+  const slamMapName = ref('')
+  const buildingProgress = ref(0)
+
+  // 导航状态
+  const isNavigating = ref(false)
+  const currentTask = ref(null)
+
+  // ==================== 录制操作 ====================
+
+  async function startRecord(mapName) {
+    try {
+      recordingMapName.value = mapName
+      isRecording.value = true
+      const res = await controlApi.startRecord(deviceId, mapName)
+      if (res.code === 200) {
+        ElMessage.success('开始录制')
+      }
+      return res
+    } catch (error) {
+      isRecording.value = false
+      ElMessage.error('开始录制失败')
+      throw error
+    }
+  }
+
+  async function stopRecord(mapName) {
+    try {
+      const res = await controlApi.stopRecord(deviceId, mapName)
+      if (res.code === 200) {
+        ElMessage.success('停止录制')
+        isRecording.value = false
+        recordingMapName.value = ''
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('停止录制失败')
+      throw error
+    }
+  }
+
+  // ==================== 构建地图 ====================
+
+  async function startBuild(mapName, buildSteps) {
+    try {
+      buildingMapName.value = mapName
+      isBuilding.value = true
+      buildingProgress.value = 0
+      const res = await controlApi.startBuild(deviceId, mapName, buildSteps)
+      if (res.code === 200) {
+        ElMessage.success('开始构建地图')
+      }
+      return res
+    } catch (error) {
+      isBuilding.value = false
+      ElMessage.error('开始构建失败')
+      throw error
+    }
+  }
+
+  async function stopBuild() {
+    try {
+      const res = await controlApi.stopBuild(deviceId)
+      if (res.code === 200) {
+        ElMessage.success('停止构建')
+        isBuilding.value = false
+        buildingMapName.value = ''
+        buildingProgress.value = 0
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('停止构建失败')
+      throw error
+    }
+  }
+  async function completedBuild(mapName, funcName) {
+    try {
+      const res = await controlApi.completedBuild(mapName, funcName)
+      return res
+    } catch (error) {
+      ElMessage.error('构建失败')
+      throw error
+    }
+  }
+
+  function updateBuildingProgress(progress) {
+    buildingProgress.value = progress
+  }
+
+  // ==================== 实时SLAM ====================
+
+  async function startSlam(mapName) {
+    try {
+      slamMapName.value = mapName
+      isSlaming.value = true
+      const res = await controlApi.startSlam(deviceId, mapName)
+      if (res.code === 200) {
+        ElMessage.success('开始实时建图')
+      }
+      return res
+    } catch (error) {
+      isSlaming.value = false
+      ElMessage.error('开始实时建图失败')
+      throw error
+    }
+  }
+
+  async function stopSlam() {
+    try {
+      const res = await controlApi.stopSlam(deviceId)
+      if (res.code === 200) {
+        ElMessage.success('停止实时建图')
+        isSlaming.value = false
+        // slamMapName.value = ''
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('停止实时建图失败')
+      throw error
+    }
+  }
+
+  // ==================== 导航操作 ====================
+
+  async function startNavigation(mapName) {
+    try {
+      const res = await controlApi.startNavigation(deviceId, mapName)
+      if (res.code === 200) {
+        ElMessage.success('开启导航')
+        isNavigating.value = true
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('开启导航失败')
+      throw error
+    }
+  }
+
+  async function stopNavigation() {
+    try {
+      const res = await controlApi.stopNavigation(deviceId)
+      if (res.code === 200) {
+        ElMessage.success('关闭导航')
+        isNavigating.value = false
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('关闭导航失败')
+      throw error
+    }
+  }
+
+  async function restartNavigation(mapName) {
+    try {
+      const res = await controlApi.restartNavigation(deviceId, mapName)
+      if (res.code === 200) {
+        ElMessage.success('重启导航')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('重启导航失败')
+      throw error
+    }
+  }
+
+  // ==================== 定位操作 ====================
+
+  async function initPose(x, y, yaw) {
+    try {
+      const res = await controlApi.initPose(deviceId, x, y, yaw)
+      if (res.code === 200) {
+        ElMessage.success('位姿初始化请求已发送')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('位姿初始化失败')
+      throw error
+    }
+  }
+
+  async function initPoseByNid(nid) {
+    try {
+      const res = await controlApi.initPoseByNid(deviceId, nid)
+      if (res.code === 200) {
+        ElMessage.success('位姿初始化请求已发送')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('位姿初始化失败')
+      throw error
+    }
+  }
+
+  // ==================== 任务操作 ====================
+
+  async function gotoTargetByNid(mapName, nid) {
+    try {
+      const res = await controlApi.gotoTargetByNid(deviceId, mapName, nid)
+      if (res.code === 200) {
+        ElMessage.info('前往目标点请求已发送')
+        currentTask.value = { mapName, nid }
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('前往目标点失败')
+      throw error
+    }
+  }
+
+  async function gotoTargetByCoord(mapName, x, y) {
+    try {
+      const res = await controlApi.gotoTargetByCoord(deviceId, mapName, x, y)
+      if (res.code === 200) {
+        ElMessage.info('前往目标点请求已发送')
+        currentTask.value = { mapName, x, y }
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('前往目标点失败')
+      throw error
+    }
+  }
+
+  async function pauseTask() {
+    try {
+      const res = await controlApi.pauseTask(deviceId)
+      if (res.code === 200) {
+        ElMessage.info('任务已暂停')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('暂停任务失败')
+      throw error
+    }
+  }
+
+  async function resumeTask() {
+    try {
+      const res = await controlApi.resumeTask(deviceId)
+      if (res.code === 200) {
+        ElMessage.info('任务已继续')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('继续任务失败')
+      throw error
+    }
+  }
+
+  async function cancelTask() {
+    try {
+      const res = await controlApi.cancelTask(deviceId)
+      if (res.code === 200) {
+        ElMessage.info('任务已取消')
+        currentTask.value = null
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('取消任务失败')
+      throw error
+    }
+  }
+
+  // ==================== 急停 ====================
+
+  async function emergencyStop(type = 'soft') {
+    try {
+      const res = await controlApi.emergencyStop(deviceId, type)
+      if (res.code === 200) {
+        ElMessage.warning('急停指令已发送')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('急停失败')
+      throw error
+    }
+  }
+
+  async function releaseEmergencyStop() {
+    try {
+      const res = await controlApi.releaseEmergencyStop(deviceId)
+      if (res.code === 200) {
+        ElMessage.success('已释放急停')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('释放急停失败')
+      throw error
+    }
+  }
+
+  // ==================== 标定 ====================
+
+  async function executeCalibration(mapName, calibrationPoints) {
+    try {
+      const res = await controlApi.executeCalibration(deviceId, mapName, calibrationPoints)
+      if (res.code === 200) {
+        ElMessage.success('标定请求已发送')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('标定失败')
+      throw error
+    }
+  }
+
+  return {
+    // 状态
+    isRecording,
+    isBuilding,
+    isSlaming,
+    isNavigating,
+    recordingMapName,
+    buildingMapName,
+    slamMapName,
+    buildingProgress,
+    currentTask,
+    
+    // 录制
+    startRecord,
+    stopRecord,
+    
+    // 构建
+    startBuild,
+    stopBuild,
+    completedBuild,
+    updateBuildingProgress,
+    
+    // 实时SLAM
+    startSlam,
+    stopSlam,
+    
+    // 导航
+    startNavigation,
+    stopNavigation,
+    restartNavigation,
+    
+    // 定位
+    initPose,
+    initPoseByNid,
+    
+    // 任务
+    gotoTargetByNid,
+    gotoTargetByCoord,
+    pauseTask,
+    resumeTask,
+    cancelTask,
+    
+    // 急停
+    emergencyStop,
+    releaseEmergencyStop,
+    
+    // 标定
+    executeCalibration
+  }
+}
+
+export function useRoadmap() {
+  const loading = ref(false)
+  const roadmapData = ref(null)
+  const pathList = ref([])
+
+  // 获取路网数据
+  async function fetchRoadmap(mapName) {
+    loading.value = true
+    try {
+      const res = await mapApi.getRoadMapGeoJson(mapName)
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || res.code === 200) {
+        roadmapData.value = responseData.data
+      } else {
+        roadmapData.value = null
+      }
+      return res
+    } catch (error) {
+      console.error('获取路网数据失败:', error)
+      ElMessage.error('获取路网数据失败')
+      roadmapData.value = null
+      throw error
+    } finally {
+      loading.value = false
+    }
+  }
+
+  // 保存路网数据
+  async function saveRoadmap(mapName, geoJson) {
+    loading.value = true
+    try {
+      const res = await mapApi.saveRoadMapGeoJson({ map: mapName, geojson: geoJson })
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || res.code === 200) {
+        ElMessage.success('保存成功')
+      } else {
+        ElMessage.error(responseData.msg || res.msg || '保存失败')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('保存失败')
+      throw error
+    } finally {
+      loading.value = false
+    }
+  }
+
+  // 获取任务路线列表
+  async function fetchPathList(mapName) {
+    try {
+      const res = await mapApi.listPath(mapName)
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (res.code === 200 || responseData.status === true) {
+        pathList.value = responseData.data || responseData.paths || []
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('获取路线列表失败')
+      throw error
+    }
+  }
+
+  // 添加路线
+  async function addPath(data) {
+    try {
+      const res = await mapApi.addPath(data)
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || res.code === 200) {
+        ElMessage.success('添加成功')
+        await fetchPathList(data.map)
+      } else {
+        ElMessage.error(responseData.msg || res.msg || '添加失败')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('添加失败')
+      throw error
+    }
+  }
+
+  // 删除路线
+  async function removePath(mapName, pathName) {
+    try {
+      const res = await mapApi.deletePath({ map: mapName, path: pathName })
+
+      // 兼容处理
+      const responseData = res.data !== undefined ? res.data : res
+
+      if (responseData.status === true || res.code === 200) {
+        ElMessage.success('删除成功')
+        pathList.value = pathList.value.filter(p => p.name !== pathName)
+      } else {
+        ElMessage.error(responseData.msg || res.msg || '删除失败')
+      }
+      return res
+    } catch (error) {
+      ElMessage.error('删除失败')
+      throw error
+    }
+  }
+
+  return {
+    loading,
+    roadmapData,
+    pathList,
+    fetchRoadmap,
+    saveRoadmap,
+    fetchPathList,
+    addPath,
+    removePath
+  }
+}

+ 151 - 0
src/composables/useWebSocket.js

@@ -0,0 +1,151 @@
+/**
+ * WebSocket组合式函数
+ * 用于在Vue组件中方便地使用WebSocket实时数据
+ */
+import { ref, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
+import { initWebSocket, disconnect, subscribeToDevice, getConnectionStatus, reconnect } from '@/utils/websocket'
+
+export function useWebSocket() {
+  const isConnected = ref(false)
+  const robotPose = ref(null)
+  const taskInfo = ref(null)
+  const trajectory = ref(null)
+  const arriveEvent = ref(null)
+  const mapData = ref(null)
+  const navigationReply = ref(null)
+  const planningReply = ref(null)
+  const asmState = ref(null)
+  const rawMessages = ref({})
+
+  // 回调函数
+  let onConnectCallback = null
+  let onDisconnectCallback = null
+  let onMessageCallback = null
+  let onErrorCallback = null
+
+  // 内部消息处理函数
+  function handleMessage(type, data) {
+    switch (type) {
+      case 'pose':
+        robotPose.value = data
+        break
+      case 'task':
+        taskInfo.value = data
+        break
+      case 'trajectory':
+        trajectory.value = data
+        break
+      case 'arrive':
+        arriveEvent.value = data
+        break
+      case 'map':
+        mapData.value = data
+        break
+      case 'navigation':
+        navigationReply.value = data
+        break
+      case 'planning':
+        planningReply.value = data
+        break
+      case 'asm':
+        asmState.value = data
+        break
+      case 'raw':
+        // 存储原始消息
+        if (!rawMessages.value[data.topic]) {
+          rawMessages.value[data.topic] = []
+        }
+        rawMessages.value[data.topic].push(data)
+        break
+    }
+
+    // 触发通用消息回调
+    if (onMessageCallback) onMessageCallback(type, data)
+  }
+
+  function connect() {
+    initWebSocket({
+      onConnect: () => {
+        isConnected.value = true
+        if (onConnectCallback) onConnectCallback()
+      },
+      onDisconnect: () => {
+        isConnected.value = false
+        if (onDisconnectCallback) onDisconnectCallback()
+      },
+      onMessage: handleMessage,
+      onError: (error) => {
+        if (onErrorCallback) onErrorCallback(error)
+      }
+    })
+  }
+
+  /**
+   * 初始化 WebSocket 连接
+   * @param {Object} config 配置对象
+   * @param {string} config.deviceId 设备ID
+   * @param {Function} config.onConnect 连接成功回调
+   * @param {Function} config.onDisconnect 断开连接回调
+   * @param {Function} config.onMessage 消息接收回调
+   * @param {Function} config.onError 错误回调
+   */
+  function initWebSocketConnection(config = {}) {
+    const { deviceId, onConnect, onDisconnect, onMessage, onError } = config
+
+    onConnectCallback = onConnect
+    onDisconnectCallback = onDisconnect
+    onMessageCallback = onMessage
+    onErrorCallback = onError
+
+    initWebSocket({
+      deviceId,
+      onConnect: () => {
+        isConnected.value = true
+        if (onConnect) onConnect()
+      },
+      onDisconnect: () => {
+        isConnected.value = false
+        if (onDisconnect) onDisconnect()
+      },
+      onMessage: handleMessage,
+      onError: (error) => {
+        if (onError) onError(error)
+      }
+    })
+  }
+
+  function switchDevice(newDeviceId) {
+    subscribeToDevice(newDeviceId)
+  }
+
+  function reconnectWebSocket() {
+    reconnect()
+  }
+
+  function disconnectWebSocket() {
+    disconnect()
+  }
+
+  return {
+    // 连接状态
+    isConnected,
+
+    // 实时数据
+    robotPose,
+    taskInfo,
+    trajectory,
+    arriveEvent,
+    mapData,
+    navigationReply,
+    planningReply,
+    asmState,
+    rawMessages,
+
+    // 方法
+    initWebSocket: initWebSocketConnection,
+    switchDevice,
+    reconnectWebSocket,
+    disconnect: disconnectWebSocket,
+    getConnectionStatus
+  }
+}

+ 69 - 0
src/router/modules/map.js

@@ -0,0 +1,69 @@
+/**
+ * 地图模块路由
+ * 
+ * 导航跳转路径格式:
+ * - 导航: /map/maplist/navigation/:mapName
+ * - 编辑: /map/maplist/edit/:mapName  
+ * - 标定: /map/maplist/calibration/:mapName
+ */
+import Layout from '@/layout'
+
+export function getMapRoutes(systemRoutes = []) {
+  const mapRoutes = [
+    // 地图列表(父路由)
+    {
+      path: '/map',
+      component: Layout,
+      hidden: true,
+      permissions: ['system:map:list'],
+      children: [
+        {
+          path: 'maplist',
+          component: () => import('@/views/map/maplist/index'),
+          name: 'MapList',
+          meta: { title: '地图列表', icon: 'documentation', noCache: true }
+        }
+      ]
+    },
+    // 地图导航页面
+    {
+      path: '/map/maplist/navigation/:mapName',
+      component: () => import('@/views/map/maplist/navigation'),
+      name: 'MapNavigation',
+      hidden: true,
+      permissions: ['system:map:list'],
+      meta: { title: '地图导航', icon: 'guide', noCache: true, activeMenu: '/map/maplist' }
+    },
+    // 地图编辑页面
+    {
+      path: '/map/maplist/edit/:mapName',
+      component: () => import('@/views/map/maplist/edit'),
+      name: 'MapEdit',
+      hidden: true,
+      permissions: ['system:map:list'],
+      meta: { title: '地图编辑', icon: 'edit', noCache: true, activeMenu: '/map/maplist' }
+    },
+    // 坐标系标定页面
+    {
+      path: '/map/maplist/calibration/:mapName',
+      component: () => import('@/views/map/maplist/calibration'),
+      name: 'MapCalibration',
+      hidden: true,
+      permissions: ['system:map:list'],
+      meta: { title: '坐标系标定', icon: 'marker', noCache: true, activeMenu: '/map/maplist' }
+    },
+    // VSLAM实时建图
+    {
+      path: '/map/vslam/:mapName',
+      component: () => import('@/views/map/vslam/index'),
+      name: 'VSlamPreview',
+      hidden: true,
+      permissions: ['system:map:list'],
+      meta: { title: 'VSLAM实时建图', icon: '3d', noCache: true, activeMenu: '/map/maplist' }
+    }
+  ]
+
+  return mapRoutes
+}
+
+export default getMapRoutes

+ 20 - 1
src/store/modules/permission.js

@@ -1,6 +1,7 @@
 import auth from '@/plugins/auth'
 import router, { constantRoutes, dynamicRoutes } from '@/router'
 import { getRouters } from '@/api/menu'
+import { getMapRoutes } from '@/router/modules/map'
 import Layout from '@/layout/index'
 import ParentView from '@/components/ParentView'
 import InnerLink from '@/layout/components/InnerLink'
@@ -32,8 +33,21 @@ const usePermissionStore = defineStore(
       setSidebarRouters(routes) {
         this.sidebarRouters = routes
       },
+      // 加载地图模块路由
+      loadMapRoutes() {
+        const mapRoutes = getMapRoutes()
+        mapRoutes.forEach(route => {
+          if (!router.hasRoute(route.name)) {
+            router.addRoute(route)
+          }
+        })
+        console.log('[Permission] 地图模块路由已加载:', mapRoutes.map(r => r.path))
+      },
       generateRoutes(roles) {
-        return new Promise(resolve => {
+        return new Promise((resolve) => {
+          // 先加载地图模块路由
+          this.loadMapRoutes()
+          
           // 向后端请求路由数据
           getRouters().then(res => {
             const sdata = JSON.parse(JSON.stringify(res.data))
@@ -44,11 +58,16 @@ const usePermissionStore = defineStore(
             const defaultRoutes = filterAsyncRouter(defaultData)
             const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
             asyncRoutes.forEach(route => { router.addRoute(route) })
+            
             this.setRoutes(rewriteRoutes)
             this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
             this.setDefaultRoutes(sidebarRoutes)
             this.setTopbarRoutes(defaultRoutes)
             resolve(rewriteRoutes)
+          }).catch(() => {
+            // 如果后端请求失败,地图模块路由已在前面加载
+            this.setRoutes([])
+            resolve([])
           })
         })
       }

+ 397 - 0
src/styles/index.scss

@@ -0,0 +1,397 @@
+/**
+ * 主样式入口文件
+ * 整合所有样式文件的导入
+ * @author 前端架构师
+ */
+
+/* ===== 设计令牌 ===== */
+@import './tokens.scss';
+
+/* ===== Element UI 主题覆盖 ===== */
+@import './overrides-element.scss';
+
+/* ===== 基础工具类 ===== */
+@import './utilities.scss';
+
+/* ===== 全局基础样式重置 ===== */
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+html {
+  font-family: var(--font-family-sans);
+  line-height: var(--line-height-normal);
+  -webkit-text-size-adjust: 100%;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-rendering: optimizeLegibility;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  background-color: var(--color-bg-primary);
+  color: var(--color-text-primary);
+  font-size: var(--font-size-base);
+  font-weight: var(--font-weight-normal);
+  line-height: var(--line-height-normal);
+  transition: background-color var(--duration-200) var(--ease-out),
+              color var(--duration-200) var(--ease-out);
+}
+
+/* ===== 滚动条美化 ===== */
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+::-webkit-scrollbar-track {
+  background: var(--color-bg-tertiary);
+  border-radius: var(--radius-full);
+}
+
+::-webkit-scrollbar-thumb {
+  background: var(--color-border-primary);
+  border-radius: var(--radius-full);
+  transition: background-color var(--duration-200) var(--ease-out);
+  
+  &:hover {
+    background: var(--color-text-quaternary);
+  }
+}
+
+::-webkit-scrollbar-corner {
+  background: var(--color-bg-tertiary);
+}
+
+/* Firefox 滚动条 */
+* {
+  scrollbar-width: thin;
+  scrollbar-color: var(--color-border-primary) var(--color-bg-tertiary);
+}
+
+/* ===== 选择文本样式 ===== */
+::selection {
+  background: var(--color-primary-200);
+  color: var(--color-primary-900);
+}
+
+::-moz-selection {
+  background: var(--color-primary-200);
+  color: var(--color-primary-900);
+}
+
+/* ===== 焦点样式 ===== */
+:focus {
+  outline: 2px solid var(--color-primary);
+  outline-offset: 2px;
+}
+
+:focus:not(:focus-visible) {
+  outline: none;
+}
+
+:focus-visible {
+  outline: 2px solid var(--color-primary);
+  outline-offset: 2px;
+}
+
+/* ===== 链接样式 ===== */
+a {
+  color: var(--color-primary);
+  text-decoration: none;
+  transition: color var(--duration-200) var(--ease-out);
+  
+  &:hover {
+    color: var(--color-primary-light);
+    text-decoration: underline;
+  }
+  
+  &:focus {
+    color: var(--color-primary-dark);
+  }
+}
+
+/* ===== 标题样式 ===== */
+h1, h2, h3, h4, h5, h6 {
+  margin: 0 0 var(--spacing-4) 0;
+  font-weight: var(--font-weight-semibold);
+  line-height: var(--line-height-tight);
+  color: var(--color-text-primary);
+}
+
+h1 {
+  font-size: var(--font-size-3xl);
+  font-weight: var(--font-weight-bold);
+}
+
+h2 {
+  font-size: var(--font-size-2xl);
+}
+
+h3 {
+  font-size: var(--font-size-xl);
+}
+
+h4 {
+  font-size: var(--font-size-lg);
+}
+
+h5 {
+  font-size: var(--font-size-base);
+}
+
+h6 {
+  font-size: var(--font-size-sm);
+}
+
+/* ===== 段落样式 ===== */
+p {
+  margin: 0 0 var(--spacing-4) 0;
+  line-height: var(--line-height-relaxed);
+  color: var(--color-text-secondary);
+}
+
+/* ===== 列表样式 ===== */
+ul, ol {
+  margin: 0 0 var(--spacing-4) 0;
+  padding-left: var(--spacing-6);
+  
+  li {
+    margin-bottom: var(--spacing-1);
+    line-height: var(--line-height-relaxed);
+    color: var(--color-text-secondary);
+  }
+}
+
+/* ===== 分割线样式 ===== */
+hr {
+  border: none;
+  height: 1px;
+  background: var(--color-divider);
+  margin: var(--spacing-6) 0;
+}
+
+/* ===== 代码样式 ===== */
+code {
+  font-family: var(--font-family-mono);
+  font-size: 0.875em;
+  background: var(--color-bg-tertiary);
+  color: var(--color-text-primary);
+  padding: var(--spacing-1) var(--spacing-2);
+  border-radius: var(--radius-sm);
+  border: 1px solid var(--color-border-secondary);
+}
+
+pre {
+  font-family: var(--font-family-mono);
+  font-size: var(--font-size-sm);
+  background: var(--color-bg-tertiary);
+  color: var(--color-text-primary);
+  padding: var(--spacing-4);
+  border-radius: var(--radius-lg);
+  border: 1px solid var(--color-border-primary);
+  overflow-x: auto;
+  margin: var(--spacing-4) 0;
+  
+  code {
+    background: none;
+    border: none;
+    padding: 0;
+    font-size: inherit;
+  }
+}
+
+/* ===== 引用样式 ===== */
+blockquote {
+  margin: var(--spacing-6) 0;
+  padding: var(--spacing-4) var(--spacing-6);
+  border-left: 4px solid var(--color-primary);
+  background: var(--color-primary-50);
+  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
+  
+  p {
+    margin: 0;
+    color: var(--color-text-primary);
+    font-style: italic;
+  }
+}
+
+/* ===== 表格样式 ===== */
+table {
+  width: 100%;
+  border-collapse: collapse;
+  margin: var(--spacing-6) 0;
+  background: var(--color-bg-card);
+  border-radius: var(--radius-lg);
+  overflow: hidden;
+  box-shadow: var(--shadow-card);
+}
+
+th, td {
+  padding: var(--spacing-3) var(--spacing-4);
+  text-align: left;
+  border-bottom: 1px solid var(--color-border-secondary);
+}
+
+th {
+  background: var(--color-bg-tertiary);
+  font-weight: var(--font-weight-semibold);
+  color: var(--color-text-primary);
+}
+
+td {
+  color: var(--color-text-secondary);
+}
+
+tr:last-child td {
+  border-bottom: none;
+}
+
+tr:hover {
+  background: var(--color-primary-50);
+}
+
+/* ===== 图片样式 ===== */
+img {
+  max-width: 100%;
+  height: auto;
+  border-radius: var(--radius-md);
+}
+
+/* ===== 表单元素基础样式 ===== */
+input, textarea, select, button {
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+/* ===== 禁用状态样式 ===== */
+[disabled] {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+/* ===== 隐藏元素样式 ===== */
+[hidden] {
+  display: none !important;
+}
+
+/* ===== 可访问性样式 ===== */
+.visually-hidden {
+  position: absolute !important;
+  width: 1px !important;
+  height: 1px !important;
+  padding: 0 !important;
+  margin: -1px !important;
+  overflow: hidden !important;
+  clip: rect(0, 0, 0, 0) !important;
+  white-space: nowrap !important;
+  border: 0 !important;
+}
+
+/* 跳转到主内容链接 */
+.skip-link {
+  position: absolute;
+  top: -40px;
+  left: var(--spacing-4);
+  background: var(--color-primary);
+  color: var(--color-text-inverse);
+  padding: var(--spacing-2) var(--spacing-4);
+  border-radius: var(--radius-md);
+  text-decoration: none;
+  font-weight: var(--font-weight-medium);
+  z-index: var(--z-index-toast);
+  transition: top var(--duration-200) var(--ease-out);
+  
+  &:focus {
+    top: var(--spacing-4);
+  }
+}
+
+/* ===== 减少动画偏好支持 ===== */
+@media (prefers-reduced-motion: reduce) {
+  *,
+  *::before,
+  *::after {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+    scroll-behavior: auto !important;
+  }
+}
+
+/* ===== 高对比度模式支持 ===== */
+@media (prefers-contrast: high) {
+  :root {
+    --color-border-primary: rgba(0, 0, 0, 0.3);
+    --color-border-secondary: rgba(0, 0, 0, 0.2);
+    --shadow-card: 0 4px 8px rgba(0, 0, 0, 0.3);
+  }
+  
+  html.dark {
+    --color-border-primary: rgba(255, 255, 255, 0.4);
+    --color-border-secondary: rgba(255, 255, 255, 0.3);
+    --shadow-card: 0 4px 8px rgba(0, 0, 0, 0.5);
+  }
+}
+
+/* ===== 打印样式 ===== */
+@media print {
+  * {
+    background: transparent !important;
+    color: black !important;
+    box-shadow: none !important;
+    text-shadow: none !important;
+  }
+  
+  a,
+  a:visited {
+    text-decoration: underline;
+  }
+  
+  a[href]:after {
+    content: " (" attr(href) ")";
+  }
+  
+  abbr[title]:after {
+    content: " (" attr(title) ")";
+  }
+  
+  pre,
+  blockquote {
+    border: 1px solid #999;
+    page-break-inside: avoid;
+  }
+  
+  thead {
+    display: table-header-group;
+  }
+  
+  tr,
+  img {
+    page-break-inside: avoid;
+  }
+  
+  img {
+    max-width: 100% !important;
+  }
+  
+  p,
+  h2,
+  h3 {
+    orphans: 3;
+    widows: 3;
+  }
+  
+  h2,
+  h3 {
+    page-break-after: avoid;
+  }
+  
+  .card {
+    border: 1px solid #ccc;
+  }
+}

+ 1078 - 0
src/styles/overrides-element.scss

@@ -0,0 +1,1078 @@
+/**
+ * Element UI 主题覆盖
+ * 使用设计令牌覆盖 Element UI 默认样式
+ * @author 前端架构师
+ */
+
+/* ===== 全局基础覆盖 ===== */
+
+/* 重置 Element UI 的 CSS 变量 */
+:root {
+  /* Element UI 主色调覆盖 */
+  --el-color-primary: var(--color-primary);
+  --el-color-success: var(--color-success);
+  --el-color-warning: var(--color-warning);
+  --el-color-danger: var(--color-danger);
+  --el-color-info: var(--color-info);
+
+  /* Element UI 文本颜色覆盖 */
+  --el-text-color-primary: var(--color-text-primary);
+  --el-text-color-regular: var(--color-text-secondary);
+  --el-text-color-secondary: var(--color-text-tertiary);
+  --el-text-color-placeholder: var(--color-text-placeholder);
+  --el-text-color-disabled: var(--color-text-disabled);
+
+  /* Element UI 背景色覆盖 */
+  --el-bg-color: var(--color-bg-primary);
+  --el-bg-color-page: var(--color-bg-secondary);
+
+  /* Element UI 边框覆盖 */
+  --el-border-color: var(--color-border-primary);
+  --el-border-color-light: var(--color-border-secondary);
+  --el-border-color-lighter: var(--color-border-tertiary);
+  --el-border-color-extra-light: var(--color-border-tertiary);
+
+  /* Element UI 圆角覆盖 */
+  --el-border-radius-base: var(--radius-lg);
+  --el-border-radius-small: var(--radius-md);
+  --el-border-radius-round: var(--radius-full);
+
+  /* Element UI 阴影覆盖 */
+  --el-box-shadow: var(--shadow-card);
+  --el-box-shadow-light: var(--shadow-sm);
+  --el-box-shadow-base: var(--shadow-base);
+  --el-box-shadow-dark: var(--shadow-lg);
+
+  /* Element UI 字体覆盖 */
+  --el-font-size-extra-small: var(--font-size-xs);
+  --el-font-size-small: var(--font-size-sm);
+  --el-font-size-base: var(--font-size-base);
+  --el-font-size-medium: var(--font-size-lg);
+  --el-font-size-large: var(--font-size-xl);
+  --el-font-size-extra-large: var(--font-size-2xl);
+}
+
+/* ===== 按钮组件覆盖 ===== */
+.el-button {
+  border-radius: var(--radius-lg);
+  font-weight: var(--font-weight-medium);
+  transition: all var(--duration-200) var(--ease-out);
+  border-width: 1px;
+  position: relative;
+  overflow: hidden;
+
+  /* 按钮悬停效果 */
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
+    transition: left var(--duration-300) var(--ease-out);
+  }
+
+  &:hover::before {
+    left: 100%;
+  }
+
+  /* 主要按钮 */
+  &.el-button--primary {
+    background: var(--color-primary);
+    border-color: var(--color-primary);
+    box-shadow: var(--shadow-sm);
+
+    &:hover,
+    &:focus {
+      background: var(--color-primary-light);
+      border-color: var(--color-primary-light);
+      box-shadow: var(--shadow-md);
+      transform: translateY(-1px);
+    }
+
+    &:active {
+      transform: translateY(0);
+      box-shadow: var(--shadow-sm);
+    }
+  }
+
+  /* 成功按钮 */
+  &.el-button--success {
+    background: var(--color-success);
+    border-color: var(--color-success);
+    
+    &:hover,
+    &:focus {
+      background: var(--color-success-light);
+      border-color: var(--color-success-light);
+    }
+  }
+
+  /* 警告按钮 */
+  &.el-button--warning {
+    background: var(--color-warning);
+    border-color: var(--color-warning);
+    
+    &:hover,
+    &:focus {
+      background: var(--color-warning-light);
+      border-color: var(--color-warning-light);
+    }
+  }
+
+  /* 危险按钮 */
+  &.el-button--danger {
+    background: var(--color-danger);
+    border-color: var(--color-danger);
+    
+    &:hover,
+    &:focus {
+      background: var(--color-danger-light);
+      border-color: var(--color-danger-light);
+    }
+  }
+
+  /* 默认按钮 */
+  &.el-button--default {
+    background: var(--color-bg-card);
+    border-color: var(--color-border-primary);
+    color: var(--color-text-primary);
+    
+    &:hover,
+    &:focus {
+      background: var(--color-bg-tertiary);
+      border-color: var(--color-primary);
+      color: var(--color-primary);
+    }
+  }
+
+  /* 文本按钮 */
+  &.el-button--text {
+    background: transparent;
+    border: none;
+    color: var(--color-primary);
+    
+    &:hover,
+    &:focus {
+      background: var(--color-primary-50);
+      color: var(--color-primary-dark);
+    }
+  }
+
+  /* 禁用状态 */
+  &.is-disabled {
+    background: var(--color-bg-tertiary) !important;
+    border-color: var(--color-border-tertiary) !important;
+    color: var(--color-text-disabled) !important;
+    cursor: not-allowed;
+    transform: none !important;
+    box-shadow: none !important;
+
+    &::before {
+      display: none;
+    }
+  }
+
+  /* 小尺寸按钮 */
+  &.el-button--small {
+    border-radius: var(--radius-md);
+    font-size: var(--font-size-sm);
+  }
+
+  /* 迷你按钮 */
+  &.el-button--mini {
+    border-radius: var(--radius-base);
+    font-size: var(--font-size-xs);
+  }
+}
+
+/* 按钮组 */
+.el-button-group {
+  .el-button {
+    &:first-child {
+      border-top-right-radius: 0;
+      border-bottom-right-radius: 0;
+    }
+    
+    &:last-child {
+      border-top-left-radius: 0;
+      border-bottom-left-radius: 0;
+    }
+    
+    &:not(:first-child):not(:last-child) {
+      border-radius: 0;
+    }
+  }
+}
+
+/* ===== 输入框组件覆盖 ===== */
+.el-input {
+  .el-input__inner {
+    background: var(--color-bg-card);
+    border: 1px solid var(--color-border-primary);
+    border-radius: var(--radius-lg);
+    color: var(--color-text-primary);
+    font-size: var(--font-size-base);
+    transition: all var(--duration-200) var(--ease-out);
+    padding: 0 var(--spacing-3);
+
+    &::placeholder {
+      color: var(--color-text-placeholder);
+    }
+
+    &:hover {
+      border-color: var(--color-primary-light);
+    }
+
+    &:focus {
+      border-color: var(--color-primary);
+      box-shadow: 0 0 0 2px var(--color-primary-100);
+      outline: none;
+    }
+  }
+
+  /* 输入框禁用状态 */
+  &.is-disabled {
+    .el-input__inner {
+      background: var(--color-bg-tertiary);
+      border-color: var(--color-border-tertiary);
+      color: var(--color-text-disabled);
+      cursor: not-allowed;
+    }
+  }
+
+  /* 输入框错误状态 */
+  &.is-error {
+    .el-input__inner {
+      border-color: var(--color-danger);
+      
+      &:focus {
+        box-shadow: 0 0 0 2px var(--color-danger-100);
+      }
+    }
+  }
+
+  /* 输入框成功状态 */
+  &.is-success {
+    .el-input__inner {
+      border-color: var(--color-success);
+    }
+  }
+
+  /* 输入框图标 */
+  .el-input__prefix,
+  .el-input__suffix {
+    color: var(--color-text-tertiary);
+  }
+
+  /* 小尺寸输入框 */
+  &.el-input--small {
+    .el-input__inner {
+      border-radius: var(--radius-md);
+    }
+  }
+
+  /* 迷你输入框 */
+  &.el-input--mini {
+    .el-input__inner {
+      border-radius: var(--radius-base);
+    }
+  }
+}
+
+/* 文本域 */
+.el-textarea {
+  .el-textarea__inner {
+    background: var(--color-bg-card);
+    border: 1px solid var(--color-border-primary);
+    border-radius: var(--radius-lg);
+    color: var(--color-text-primary);
+    font-family: var(--font-family-sans);
+    transition: all var(--duration-200) var(--ease-out);
+
+    &::placeholder {
+      color: var(--color-text-placeholder);
+    }
+
+    &:hover {
+      border-color: var(--color-primary-light);
+    }
+
+    &:focus {
+      border-color: var(--color-primary);
+      box-shadow: 0 0 0 2px var(--color-primary-100);
+      outline: none;
+    }
+  }
+}
+
+/* ===== 选择器组件覆盖 ===== */
+.el-select {
+  .el-input__inner {
+    cursor: pointer;
+  }
+
+  .el-input__suffix {
+    transition: transform var(--duration-200) var(--ease-out);
+  }
+
+  &.is-focus {
+    .el-input__suffix {
+      transform: rotate(180deg);
+    }
+  }
+}
+
+/* 选择器下拉面板 */
+.el-select-dropdown {
+  background: var(--color-bg-card);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-lg);
+  box-shadow: var(--shadow-xl);
+  margin-top: var(--spacing-1);
+
+  .el-select-dropdown__item {
+    color: var(--color-text-primary);
+    padding: var(--spacing-2) var(--spacing-3);
+    transition: all var(--duration-150) var(--ease-out);
+
+    &:hover {
+      background: var(--color-primary-50);
+      color: var(--color-primary);
+    }
+
+    &.selected {
+      background: var(--color-primary);
+      color: var(--color-text-inverse);
+      font-weight: var(--font-weight-medium);
+    }
+  }
+}
+
+/* ===== 表单组件覆盖 ===== */
+.el-form {
+  .el-form-item {
+    margin-bottom: var(--spacing-6);
+
+    &__label {
+      color: var(--color-text-primary);
+      font-weight: var(--font-weight-medium);
+      line-height: var(--line-height-normal);
+      padding-bottom: var(--spacing-2);
+    }
+
+    &__content {
+      line-height: var(--line-height-normal);
+    }
+
+    &__error {
+      color: var(--color-danger);
+      font-size: var(--font-size-sm);
+      line-height: var(--line-height-tight);
+      padding-top: var(--spacing-1);
+    }
+
+    /* 必填标识 */
+    &.is-required {
+      .el-form-item__label::before {
+        color: var(--color-danger);
+        margin-right: var(--spacing-1);
+      }
+    }
+
+    /* 表单项错误状态 */
+    &.is-error {
+      .el-form-item__label {
+        color: var(--color-danger);
+      }
+    }
+  }
+}
+
+/* ===== 对话框组件覆盖 ===== */
+.el-dialog {
+  background: var(--color-bg-card);
+  border-radius: var(--radius-2xl);
+  box-shadow: var(--shadow-2xl);
+  border: 1px solid var(--color-border-primary);
+
+  &__header {
+    background: var(--color-bg-card);
+    border-bottom: 1px solid var(--color-border-secondary);
+    border-radius: var(--radius-2xl) var(--radius-2xl) 0 0;
+    padding: var(--spacing-6) var(--spacing-6) var(--spacing-4);
+  }
+
+  &__title {
+    color: var(--color-text-primary);
+    font-size: var(--font-size-xl);
+    font-weight: var(--font-weight-semibold);
+    line-height: var(--line-height-tight);
+  }
+
+  &__headerbtn {
+    top: var(--spacing-4);
+    right: var(--spacing-4);
+    width: var(--spacing-8);
+    height: var(--spacing-8);
+    background: var(--color-bg-tertiary);
+    border-radius: var(--radius-full);
+    transition: all var(--duration-200) var(--ease-out);
+
+    &:hover {
+      background: var(--color-danger-100);
+    }
+
+    .el-dialog__close {
+      color: var(--color-text-tertiary);
+      font-size: var(--font-size-lg);
+
+      &:hover {
+        color: var(--color-danger);
+      }
+    }
+  }
+
+  &__body {
+    background: var(--color-bg-card);
+    color: var(--color-text-primary);
+    padding: var(--spacing-6);
+    line-height: var(--line-height-relaxed);
+  }
+
+  &__footer {
+    background: var(--color-bg-card);
+    border-top: 1px solid var(--color-border-secondary);
+    border-radius: 0 0 var(--radius-2xl) var(--radius-2xl);
+    padding: var(--spacing-4) var(--spacing-6);
+    text-align: right;
+
+    .el-button {
+      margin-left: var(--spacing-3);
+    }
+  }
+}
+
+/* 对话框遮罩 */
+.el-dialog__wrapper {
+  background: var(--color-bg-mask);
+  backdrop-filter: blur(4px);
+}
+
+/* ===== 抽屉组件覆盖 ===== */
+.el-drawer {
+  background: var(--color-bg-card);
+  box-shadow: var(--shadow-2xl);
+
+  &__header {
+    background: var(--color-bg-card);
+    border-bottom: 1px solid var(--color-border-secondary);
+    margin-bottom: 0;
+    padding: var(--spacing-6) var(--spacing-6) var(--spacing-4);
+
+    span {
+      color: var(--color-text-primary);
+      font-size: var(--font-size-xl);
+      font-weight: var(--font-weight-semibold);
+      line-height: var(--line-height-tight);
+    }
+  }
+
+  &__close-btn {
+    color: var(--color-text-tertiary);
+    font-size: var(--font-size-xl);
+    top: var(--spacing-6);
+    right: var(--spacing-6);
+    width: var(--spacing-8);
+    height: var(--spacing-8);
+    background: var(--color-bg-tertiary);
+    border-radius: var(--radius-full);
+    transition: all var(--duration-200) var(--ease-out);
+
+    &:hover {
+      background: var(--color-danger-100);
+      color: var(--color-danger);
+    }
+  }
+
+  &__body {
+    background: var(--color-bg-card);
+    color: var(--color-text-primary);
+    padding: var(--spacing-6);
+  }
+}
+
+/* ===== 表格组件覆盖 ===== */
+.el-table {
+  background: var(--color-bg-card);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-xl);
+  overflow: hidden;
+  color: var(--color-text-primary);
+
+  /* 表头 */
+  &__header-wrapper {
+    .el-table__header {
+      th {
+        background: var(--color-bg-tertiary);
+        border-bottom: 1px solid var(--color-border-primary);
+        color: var(--color-text-primary);
+        font-weight: var(--font-weight-semibold);
+        padding: var(--spacing-4) var(--spacing-3);
+
+        &:first-child {
+          border-top-left-radius: var(--radius-xl);
+        }
+
+        &:last-child {
+          border-top-right-radius: var(--radius-xl);
+        }
+      }
+    }
+  }
+
+  /* 表体 */
+  &__body-wrapper {
+    .el-table__body {
+      tr {
+        background: var(--color-bg-card);
+        transition: background-color var(--duration-150) var(--ease-out);
+
+        &:hover {
+          background: var(--color-primary-50) !important;
+        }
+
+        td {
+          border-bottom: 1px solid var(--color-border-secondary);
+          color: var(--color-text-primary);
+          padding: var(--spacing-4) var(--spacing-3);
+        }
+
+        &:last-child td {
+          border-bottom: none;
+        }
+      }
+    }
+  }
+
+  /* 表格边框 */
+  &--border {
+    border: 1px solid var(--color-border-primary);
+
+    &::after {
+      background: var(--color-border-primary);
+    }
+
+    &::before {
+      background: var(--color-border-primary);
+    }
+
+    th,
+    td {
+      border-right: 1px solid var(--color-border-secondary);
+    }
+  }
+
+  /* 斑马纹表格 */
+  &--striped {
+    .el-table__body {
+      tr.el-table__row--striped {
+        background: var(--color-bg-secondary);
+
+        &:hover {
+          background: var(--color-primary-50) !important;
+        }
+      }
+    }
+  }
+
+  /* 空数据 */
+  &__empty-block {
+    background: var(--color-bg-card);
+  }
+
+  &__empty-text {
+    color: var(--color-text-tertiary);
+  }
+}
+
+/* 表格分页 */
+.el-pagination {
+  color: var(--color-text-primary);
+
+  .el-pager li {
+    background: var(--color-bg-card);
+    border: 1px solid var(--color-border-primary);
+    border-radius: var(--radius-md);
+    color: var(--color-text-primary);
+    margin: 0 var(--spacing-1);
+    transition: all var(--duration-200) var(--ease-out);
+
+    &:hover {
+      background: var(--color-primary-50);
+      border-color: var(--color-primary);
+      color: var(--color-primary);
+    }
+
+    &.active {
+      background: var(--color-primary);
+      border-color: var(--color-primary);
+      color: var(--color-text-inverse);
+    }
+  }
+
+  .btn-prev,
+  .btn-next {
+    background: var(--color-bg-card);
+    border: 1px solid var(--color-border-primary);
+    border-radius: var(--radius-md);
+    color: var(--color-text-primary);
+
+    &:hover {
+      background: var(--color-primary-50);
+      color: var(--color-primary);
+    }
+  }
+}
+
+/* ===== 标签页组件覆盖 ===== */
+.el-tabs {
+  &__header {
+    background: var(--color-bg-card);
+    border-bottom: 1px solid var(--color-border-primary);
+    margin: 0;
+  }
+
+  &__nav-wrap {
+    &::after {
+      background: var(--color-border-primary);
+    }
+  }
+
+  &__item {
+    color: var(--color-text-secondary);
+    font-weight: var(--font-weight-medium);
+    padding: 0 var(--spacing-5);
+    height: var(--spacing-12);
+    line-height: var(--spacing-12);
+    transition: all var(--duration-200) var(--ease-out);
+    position: relative;
+
+    &:hover {
+      color: var(--color-primary);
+    }
+
+    &.is-active {
+      color: var(--color-primary);
+      font-weight: var(--font-weight-semibold);
+
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        height: 2px;
+        background: var(--color-primary);
+        border-radius: var(--radius-full);
+      }
+    }
+  }
+
+  &__content {
+    background: var(--color-bg-card);
+    color: var(--color-text-primary);
+    // padding: var(--spacing-6);  // 保持注释(不要启用)
+  }
+
+  /* 卡片式标签页 */
+  &--card {
+    .el-tabs__header {
+      background: var(--color-bg-secondary);
+      border-bottom: none;
+    }
+
+    .el-tabs__item {
+      background: var(--color-bg-tertiary);
+      border: 1px solid var(--color-border-primary);
+      border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+      margin-right: var(--spacing-1);
+
+      &.is-active {
+        background: var(--color-bg-card);
+        border-bottom-color: var(--color-bg-card);
+        
+        &::after {
+          display: none;
+        }
+      }
+    }
+  }
+}
+
+/* ===== 消息提示覆盖 ===== */
+.el-message {
+  background: var(--color-bg-card);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-xl);
+  box-shadow: var(--shadow-xl);
+  color: var(--color-text-primary);
+  backdrop-filter: blur(8px);
+
+  &--success {
+    background: var(--color-success-50);
+    border-color: var(--color-success);
+    color: var(--color-success-dark);
+
+    .el-message__icon {
+      color: var(--color-success);
+    }
+  }
+
+  &--warning {
+    background: var(--color-warning-50);
+    border-color: var(--color-warning);
+    color: var(--color-warning-dark);
+
+    .el-message__icon {
+      color: var(--color-warning);
+    }
+  }
+
+  &--error {
+    background: var(--color-danger-50);
+    border-color: var(--color-danger);
+    color: var(--color-danger-dark);
+
+    .el-message__icon {
+      color: var(--color-danger);
+    }
+  }
+
+  &--info {
+    .el-message__icon {
+      color: var(--color-primary);
+    }
+  }
+}
+
+/* ===== 通知组件覆盖 ===== */
+.el-notification {
+  background: var(--color-bg-card);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-xl);
+  box-shadow: var(--shadow-xl);
+  backdrop-filter: blur(8px);
+
+  &__title {
+    color: var(--color-text-primary);
+    font-weight: var(--font-weight-semibold);
+  }
+
+  &__content {
+    color: var(--color-text-secondary);
+  }
+
+  &.success {
+    border-left: 4px solid var(--color-success);
+    
+    .el-notification__icon {
+      color: var(--color-success);
+    }
+  }
+
+  &.warning {
+    border-left: 4px solid var(--color-warning);
+    
+    .el-notification__icon {
+      color: var(--color-warning);
+    }
+  }
+
+  &.error {
+    border-left: 4px solid var(--color-danger);
+    
+    .el-notification__icon {
+      color: var(--color-danger);
+    }
+  }
+
+  &.info {
+    border-left: 4px solid var(--color-primary);
+    
+    .el-notification__icon {
+      color: var(--color-primary);
+    }
+  }
+}
+
+/* ===== 工具提示覆盖 ===== */
+.el-tooltip__popper {
+  background: var(--color-gray-900);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-md);
+  color: var(--color-text-inverse);
+  font-size: var(--font-size-sm);
+  box-shadow: var(--shadow-lg);
+
+  &[x-placement^="top"] {
+    .popper__arrow {
+      border-top-color: var(--color-gray-900);
+    }
+  }
+
+  &[x-placement^="bottom"] {
+    .popper__arrow {
+      border-bottom-color: var(--color-gray-900);
+    }
+  }
+
+  &[x-placement^="left"] {
+    .popper__arrow {
+      border-left-color: var(--color-gray-900);
+    }
+  }
+
+  &[x-placement^="right"] {
+    .popper__arrow {
+      border-right-color: var(--color-gray-900);
+    }
+  }
+}
+
+/* ===== 加载组件覆盖 ===== */
+.el-loading-mask {
+  background: var(--color-bg-overlay);
+  backdrop-filter: blur(2px);
+
+  .el-loading-spinner {
+    .el-loading-text {
+      color: var(--color-text-primary);
+      font-weight: var(--font-weight-medium);
+    }
+
+    .circular {
+      stroke: var(--color-primary);
+    }
+  }
+}
+
+/* ===== 进度条组件覆盖 ===== */
+.el-progress {
+  &__text {
+    color: var(--color-text-primary);
+    font-weight: var(--font-weight-medium);
+  }
+
+  &-bar {
+    &__outer {
+      background: var(--color-bg-tertiary);
+      border-radius: var(--radius-full);
+    }
+
+    &__inner {
+      background: var(--color-primary);
+      border-radius: var(--radius-full);
+      transition: width var(--duration-300) var(--ease-out);
+    }
+  }
+
+  &--success {
+    .el-progress-bar__inner {
+      background: var(--color-success);
+    }
+  }
+
+  &--warning {
+    .el-progress-bar__inner {
+      background: var(--color-warning);
+    }
+  }
+
+  &--exception {
+    .el-progress-bar__inner {
+      background: var(--color-danger);
+    }
+  }
+}
+
+/* ===== 标签组件覆盖 ===== */
+.el-tag {
+  background: var(--color-bg-tertiary);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-full);
+  color: var(--color-text-primary);
+  font-weight: var(--font-weight-medium);
+  padding: var(--spacing-1) var(--spacing-3);
+
+  &--success {
+    background: var(--color-success-100);
+    border-color: var(--color-success);
+    color: var(--color-success-dark);
+  }
+
+  &--warning {
+    background: var(--color-warning-100);
+    border-color: var(--color-warning);
+    color: var(--color-warning-dark);
+  }
+
+  &--danger {
+    background: var(--color-danger-100);
+    border-color: var(--color-danger);
+    color: var(--color-danger-dark);
+  }
+
+  &--info {
+    background: var(--color-primary-100);
+    border-color: var(--color-primary);
+    color: var(--color-primary-dark);
+  }
+
+  .el-tag__close {
+    color: inherit;
+    border-radius: var(--radius-full);
+    transition: all var(--duration-150) var(--ease-out);
+
+    &:hover {
+      background: rgba(255, 255, 255, 0.2);
+    }
+  }
+}
+
+/* ===== 开关组件覆盖 ===== */
+.el-switch {
+  &__core {
+    background: var(--color-bg-tertiary);
+    border: 1px solid var(--color-border-primary);
+    border-radius: var(--radius-full);
+    transition: all var(--duration-200) var(--ease-out);
+
+    &::after {
+      background: var(--color-bg-card);
+      border-radius: var(--radius-full);
+      box-shadow: var(--shadow-sm);
+      transition: all var(--duration-200) var(--ease-out);
+    }
+  }
+
+  &.is-checked {
+    .el-switch__core {
+      background: var(--color-primary);
+      border-color: var(--color-primary);
+    }
+  }
+
+  &__label {
+    color: var(--color-text-primary);
+    font-weight: var(--font-weight-medium);
+
+    &.is-active {
+      color: var(--color-primary);
+    }
+  }
+}
+
+/* ===== 滑块组件覆盖 ===== */
+.el-slider {
+  &__runway {
+    background: var(--color-bg-tertiary);
+    border-radius: var(--radius-full);
+  }
+
+  &__bar {
+    background: var(--color-primary);
+    border-radius: var(--radius-full);
+  }
+
+  &__button {
+    background: var(--color-bg-card);
+    border: 2px solid var(--color-primary);
+    box-shadow: var(--shadow-md);
+    transition: all var(--duration-200) var(--ease-out);
+
+    &:hover {
+      box-shadow: var(--shadow-lg);
+      transform: scale(1.1);
+    }
+  }
+
+  &__stop {
+    background: var(--color-bg-tertiary);
+  }
+}
+
+/* ===== 复选框组件覆盖 ===== */
+.el-checkbox {
+  &__input {
+    .el-checkbox__inner {
+      background: var(--color-bg-card);
+      border: 1px solid var(--color-border-primary);
+      border-radius: var(--radius-sm);
+      transition: all var(--duration-200) var(--ease-out);
+
+      &:hover {
+        border-color: var(--color-primary);
+      }
+    }
+
+    &.is-checked {
+      .el-checkbox__inner {
+        background: var(--color-primary);
+        border-color: var(--color-primary);
+
+        &::after {
+          border-color: var(--color-text-inverse);
+        }
+      }
+    }
+
+    &.is-indeterminate {
+      .el-checkbox__inner {
+        background: var(--color-primary);
+        border-color: var(--color-primary);
+
+        &::before {
+          background: var(--color-text-inverse);
+        }
+      }
+    }
+  }
+
+  &__label {
+    color: var(--color-text-primary);
+    font-weight: var(--font-weight-normal);
+  }
+}
+
+/* ===== 单选框组件覆盖 ===== */
+.el-radio {
+  &__input {
+    .el-radio__inner {
+      background: var(--color-bg-card);
+      border: 1px solid var(--color-border-primary);
+      transition: all var(--duration-200) var(--ease-out);
+
+      &:hover {
+        border-color: var(--color-primary);
+      }
+
+      &::after {
+        background: var(--color-primary);
+      }
+    }
+
+    &.is-checked {
+      .el-radio__inner {
+        background: var(--color-bg-card);
+        border-color: var(--color-primary);
+      }
+    }
+  }
+
+  &__label {
+    color: var(--color-text-primary);
+    font-weight: var(--font-weight-normal);
+  }
+}

+ 344 - 0
src/styles/tokens.scss

@@ -0,0 +1,344 @@
+/**
+ * Design Tokens - 设计令牌系统
+ * 支持明暗主题切换的 CSS 变量定义
+ * @author 前端架构师
+ */
+
+/* ===== 明亮主题(默认) ===== */
+:root {
+  /* 主色系 - 科技蓝 */
+  --color-primary: #0EA5E9;
+  --color-primary-light: #38BDF8;
+  --color-primary-dark: #0284C7;
+  --color-primary-50: #F0F9FF;
+  --color-primary-100: #E0F2FE;
+  --color-primary-200: #BAE6FD;
+  --color-primary-300: #7DD3FC;
+  --color-primary-400: #38BDF8;
+  --color-primary-500: #0EA5E9;
+  --color-primary-600: #0284C7;
+  --color-primary-700: #0369A1;
+  --color-primary-800: #075985;
+  --color-primary-900: #0C4A6E;
+  --color-primary-950: #082F49;
+
+  /* 语义化颜色 */
+  --color-success: #22C55E;
+  --color-success-light: #4ADE80;
+  --color-success-dark: #16A34A;
+  --color-success-50: #F0FDF4;
+  --color-success-100: #DCFCE7;
+  --color-success-600: #16A34A;
+
+  --color-warning: #F59E0B;
+  --color-warning-light: #FBBF24;
+  --color-warning-dark: #D97706;
+  --color-warning-50: #FFFBEB;
+  --color-warning-100: #FEF3C7;
+  --color-warning-600: #D97706;
+
+  --color-danger: #EF4444;
+  --color-danger-light: #F87171;
+  --color-danger-dark: #DC2626;
+  --color-danger-50: #FEF2F2;
+  --color-danger-100: #FEE2E2;
+  --color-danger-600: #DC2626;
+
+  --color-info: #6B7280;
+  --color-info-light: #9CA3AF;
+  --color-info-dark: #4B5563;
+
+  /* 中性色系 */
+  --color-gray-50: #F8FAFC;
+  --color-gray-100: #F1F5F9;
+  --color-gray-200: #E2E8F0;
+  --color-gray-300: #CBD5E1;
+  --color-gray-400: #94A3B8;
+  --color-gray-500: #64748B;
+  --color-gray-600: #475569;
+  --color-gray-700: #334155;
+  --color-gray-800: #1E293B;
+  --color-gray-900: #0F172A;
+  --color-gray-950: #020617;
+
+  /* 背景色 */
+  --color-bg-primary: #FFFFFF;
+  --color-bg-secondary: #F8FAFC;
+  --color-bg-tertiary: #F1F5F9;
+  --color-bg-card: #FFFFFF;
+  --color-bg-overlay: rgba(15, 23, 42, 0.8);
+  --color-bg-mask: rgba(0, 0, 0, 0.45);
+
+  /* 文本色 */
+  --color-text-primary: #0F172A;
+  --color-text-secondary: #475569;
+  --color-text-tertiary: #64748B;
+  --color-text-quaternary: #94A3B8;
+  --color-text-placeholder: #CBD5E1;
+  --color-text-disabled: #E2E8F0;
+  --color-text-inverse: #FFFFFF;
+
+  /* 边框色 */
+  --color-border-primary: rgba(2, 6, 23, 0.08);
+  --color-border-secondary: rgba(2, 6, 23, 0.06);
+  --color-border-tertiary: rgba(2, 6, 23, 0.04);
+  --color-border-focus: var(--color-primary);
+  --color-border-error: var(--color-danger);
+
+  /* 分割线 */
+  --color-divider: rgba(2, 6, 23, 0.06);
+  --color-divider-light: rgba(2, 6, 23, 0.04);
+
+  /* 圆角系统 */
+  --radius-none: 0;
+  --radius-xs: 2px;
+  --radius-sm: 4px;
+  --radius-base: 6px;
+  --radius-md: 8px;
+  --radius-lg: 12px;
+  --radius-xl: 16px;
+  --radius-2xl: 20px;
+  --radius-3xl: 24px;
+  --radius-full: 9999px;
+
+  /* 阴影系统 */
+  --shadow-xs: 0 1px 2px rgba(2, 6, 23, 0.05);
+  --shadow-sm: 0 1px 3px rgba(2, 6, 23, 0.1), 0 1px 2px rgba(2, 6, 23, 0.06);
+  --shadow-base: 0 1px 3px rgba(2, 6, 23, 0.1), 0 1px 2px rgba(2, 6, 23, 0.06);
+  --shadow-md: 0 4px 6px -1px rgba(2, 6, 23, 0.1), 0 2px 4px -1px rgba(2, 6, 23, 0.06);
+  --shadow-lg: 0 10px 15px -3px rgba(2, 6, 23, 0.1), 0 4px 6px -2px rgba(2, 6, 23, 0.05);
+  --shadow-xl: 0 20px 25px -5px rgba(2, 6, 23, 0.1), 0 10px 10px -5px rgba(2, 6, 23, 0.04);
+  --shadow-2xl: 0 25px 50px -12px rgba(2, 6, 23, 0.25);
+  --shadow-card: 0 8px 24px rgba(2, 6, 23, 0.06);
+  --shadow-card-hover: 0 12px 32px rgba(2, 6, 23, 0.12);
+  --shadow-inner: inset 0 2px 4px rgba(2, 6, 23, 0.06);
+  --shadow-none: none;
+
+  /* 间距系统 */
+  --spacing-0: 0;
+  --spacing-px: 1px;
+  --spacing-0_5: 2px;
+  --spacing-1: 4px;
+  --spacing-1_5: 6px;
+  --spacing-2: 8px;
+  --spacing-2_5: 10px;
+  --spacing-3: 12px;
+  --spacing-3_5: 14px;
+  --spacing-4: 16px;
+  --spacing-5: 20px;
+  --spacing-6: 24px;
+  --spacing-7: 28px;
+  --spacing-8: 32px;
+  --spacing-9: 36px;
+  --spacing-10: 40px;
+  --spacing-11: 44px;
+  --spacing-12: 48px;
+  --spacing-14: 56px;
+  --spacing-16: 64px;
+  --spacing-20: 80px;
+  --spacing-24: 96px;
+  --spacing-28: 112px;
+  --spacing-32: 128px;
+
+  /* 字体系统 */
+  --font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+  --font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
+
+  /* 字号系统 */
+  --font-size-xs: 12px;
+  --font-size-sm: 13px;
+  --font-size-base: 14px;
+  --font-size-lg: 16px;
+  --font-size-xl: 18px;
+  --font-size-2xl: 20px;
+  --font-size-3xl: 24px;
+  --font-size-4xl: 28px;
+  --font-size-5xl: 32px;
+
+  /* 行高系统 */
+  --line-height-none: 1;
+  --line-height-tight: 1.25;
+  --line-height-snug: 1.375;
+  --line-height-normal: 1.5;
+  --line-height-relaxed: 1.625;
+  --line-height-loose: 2;
+
+  /* 字重系统 */
+  --font-weight-thin: 100;
+  --font-weight-light: 300;
+  --font-weight-normal: 400;
+  --font-weight-medium: 500;
+  --font-weight-semibold: 600;
+  --font-weight-bold: 700;
+  --font-weight-extrabold: 800;
+  --font-weight-black: 900;
+
+  /* 层级系统 */
+  --z-index-dropdown: 1000;
+  --z-index-sticky: 1020;
+  --z-index-fixed: 1030;
+  --z-index-modal-backdrop: 1040;
+  --z-index-modal: 1050;
+  --z-index-popover: 1060;
+  --z-index-tooltip: 1070;
+  --z-index-toast: 2000;
+
+  /* 动画系统 */
+  --duration-75: 75ms;
+  --duration-100: 100ms;
+  --duration-150: 150ms;
+  --duration-200: 200ms;
+  --duration-300: 300ms;
+  --duration-500: 500ms;
+  --duration-700: 700ms;
+  --duration-1000: 1000ms;
+
+  --ease-linear: linear;
+  --ease-in: cubic-bezier(0.4, 0, 1, 1);
+  --ease-out: cubic-bezier(0, 0, 0.2, 1);
+  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+
+  /* 断点系统 */
+  --breakpoint-sm: 640px;
+  --breakpoint-md: 768px;
+  --breakpoint-lg: 1024px;
+  --breakpoint-xl: 1280px;
+  --breakpoint-2xl: 1536px;
+
+  /* 容器宽度 */
+  --container-sm: 640px;
+  --container-md: 768px;
+  --container-lg: 1024px;
+  --container-xl: 1280px;
+  --container-2xl: 1536px;
+}
+
+/* ===== 暗色主题覆盖 ===== */
+html.dark {
+  /* 背景色 - 暗色主题 */
+  --color-bg-primary: #111827;
+  --color-bg-secondary: #1F2937;
+  --color-bg-tertiary: #374151;
+  --color-bg-card: #1F2937;
+  --color-bg-overlay: rgba(17, 24, 39, 0.8);
+  --color-bg-mask: rgba(0, 0, 0, 0.6);
+
+  /* 文本色 - 暗色主题 */
+  --color-text-primary: #F9FAFB;
+  --color-text-secondary: #E5E7EB;
+  --color-text-tertiary: #D1D5DB;
+  --color-text-quaternary: #9CA3AF;
+  --color-text-placeholder: #6B7280;
+  --color-text-disabled: #4B5563;
+  --color-text-inverse: #111827;
+
+  /* 边框色 - 暗色主题 */
+  --color-border-primary: #374151;
+  --color-border-secondary: #4B5563;
+  --color-border-tertiary: #6B7280;
+
+  /* 分割线 - 暗色主题 */
+  --color-divider: #374151;
+  --color-divider-light: #4B5563;
+
+  /* 阴影系统 - 暗色主题 */
+  --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
+  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
+  --shadow-base: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
+  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
+  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.25);
+  --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
+  --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+  --shadow-card: 0 8px 24px rgba(0, 0, 0, 0.3);
+  --shadow-card-hover: 0 12px 32px rgba(0, 0, 0, 0.4);
+  --shadow-inner: inset 0 2px 4px rgba(0, 0, 0, 0.3);
+
+  /* 主色系在暗色主题下的调整 */
+  --color-primary: #38BDF8;
+  --color-primary-light: #7DD3FC;
+  --color-primary-dark: #0EA5E9;
+
+  /* 语义化颜色在暗色主题下的调整 */
+  --color-success: #4ADE80;
+  --color-success-light: #6EE7B7;
+  --color-success-dark: #22C55E;
+
+  --color-warning: #FBBF24;
+  --color-warning-light: #FCD34D;
+  --color-warning-dark: #F59E0B;
+
+  --color-danger: #F87171;
+  --color-danger-light: #FCA5A5;
+  --color-danger-dark: #EF4444;
+}
+
+/* ===== 主题切换动画 ===== */
+* {
+  transition: 
+    background-color var(--duration-200) var(--ease-out),
+    border-color var(--duration-200) var(--ease-out),
+    color var(--duration-200) var(--ease-out),
+    box-shadow var(--duration-200) var(--ease-out);
+}
+
+/* 禁用某些元素的主题切换动画 */
+*:is(
+  .el-loading-mask,
+  .el-loading-spinner,
+  .el-progress-bar__inner,
+  .el-slider__runway,
+  .el-slider__bar
+) {
+  transition: none !important;
+}
+
+/* ===== 响应式工具类 ===== */
+@media (max-width: 640px) {
+  :root {
+    --spacing-container: var(--spacing-4);
+  }
+}
+
+@media (min-width: 641px) and (max-width: 768px) {
+  :root {
+    --spacing-container: var(--spacing-6);
+  }
+}
+
+@media (min-width: 769px) {
+  :root {
+    --spacing-container: var(--spacing-8);
+  }
+}
+
+/* ===== 打印样式优化 ===== */
+@media print {
+  :root {
+    --shadow-card: none;
+    --shadow-card-hover: none;
+    --color-bg-card: transparent;
+    --color-border-primary: #000000;
+  }
+}
+
+/* ===== 高对比度模式支持 ===== */
+@media (prefers-contrast: high) {
+  :root {
+    --color-border-primary: rgba(0, 0, 0, 0.2);
+    --color-border-secondary: rgba(0, 0, 0, 0.15);
+  }
+
+  html.dark {
+    --color-border-primary: rgba(255, 255, 255, 0.3);
+    --color-border-secondary: rgba(255, 255, 255, 0.2);
+  }
+}
+
+/* ===== 减少动画偏好支持 ===== */
+@media (prefers-reduced-motion: reduce) {
+  * {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+  }
+}

+ 916 - 0
src/styles/utilities.scss

@@ -0,0 +1,916 @@
+/**
+ * 基础工具类
+ * 提供常用的样式工具类
+ * @author 前端架构师
+ */
+
+/* ===== 卡片工具类 ===== */
+.card {
+  background: var(--color-bg-card);
+  border: 1px solid var(--color-border-primary);
+  border-radius: var(--radius-lg);
+  box-shadow: var(--shadow-card);
+  padding: var(--spacing-6);
+  transition: all var(--duration-200) var(--ease-out);
+
+  &:hover {
+    box-shadow: var(--shadow-card-hover);
+    transform: translateY(-1px);
+  }
+
+  /* 卡片变体 */
+  &--flat {
+    box-shadow: none;
+    border: 1px solid var(--color-border-secondary);
+
+    &:hover {
+      box-shadow: var(--shadow-sm);
+      transform: none;
+    }
+  }
+
+  &--elevated {
+    box-shadow: var(--shadow-lg);
+
+    &:hover {
+      box-shadow: var(--shadow-xl);
+    }
+  }
+
+  &--compact {
+    padding: var(--spacing-4);
+  }
+
+  &--spacious {
+    padding: var(--spacing-8);
+  }
+
+  /* 卡片头部 */
+  &__header {
+    border-bottom: 1px solid var(--color-border-secondary);
+    margin: calc(var(--spacing-6) * -1) calc(var(--spacing-6) * -1) var(--spacing-4);
+    padding: var(--spacing-4) var(--spacing-6);
+
+    &-title {
+      color: var(--color-text-primary);
+      font-size: var(--font-size-lg);
+      font-weight: var(--font-weight-semibold);
+      margin: 0;
+      line-height: var(--line-height-tight);
+    }
+
+    &-subtitle {
+      color: var(--color-text-secondary);
+      font-size: var(--font-size-sm);
+      margin: var(--spacing-1) 0 0;
+      line-height: var(--line-height-normal);
+    }
+
+    &-extra {
+      display: flex;
+      align-items: center;
+      gap: var(--spacing-2);
+      margin-left: auto;
+    }
+  }
+
+  /* 卡片内容 */
+  &__content {
+    color: var(--color-text-primary);
+    line-height: var(--line-height-relaxed);
+  }
+
+  /* 卡片底部 */
+  &__footer {
+    border-top: 1px solid var(--color-border-secondary);
+    margin: var(--spacing-4) calc(var(--spacing-6) * -1) calc(var(--spacing-6) * -1);
+    padding: var(--spacing-4) var(--spacing-6);
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+
+/* ===== 文本工具类 ===== */
+.muted {
+  color: var(--color-text-tertiary) !important;
+}
+
+.text-primary {
+  color: var(--color-text-primary) !important;
+}
+
+.text-secondary {
+  color: var(--color-text-secondary) !important;
+}
+
+.text-tertiary {
+  color: var(--color-text-tertiary) !important;
+}
+
+.text-quaternary {
+  color: var(--color-text-quaternary) !important;
+}
+
+.text-success {
+  color: var(--color-success) !important;
+}
+
+.text-warning {
+  color: var(--color-warning) !important;
+}
+
+.text-danger {
+  color: var(--color-danger) !important;
+}
+
+.text-info {
+  color: var(--color-primary) !important;
+}
+
+/* 字体大小 */
+.text-xs {
+  font-size: var(--font-size-xs) !important;
+}
+
+.text-sm {
+  font-size: var(--font-size-sm) !important;
+}
+
+.text-base {
+  font-size: var(--font-size-base) !important;
+}
+
+.text-lg {
+  font-size: var(--font-size-lg) !important;
+}
+
+.text-xl {
+  font-size: var(--font-size-xl) !important;
+}
+
+.text-2xl {
+  font-size: var(--font-size-2xl) !important;
+}
+
+/* 字体粗细 */
+.font-thin {
+  font-weight: var(--font-weight-thin) !important;
+}
+
+.font-light {
+  font-weight: var(--font-weight-light) !important;
+}
+
+.font-normal {
+  font-weight: var(--font-weight-normal) !important;
+}
+
+.font-medium {
+  font-weight: var(--font-weight-medium) !important;
+}
+
+.font-semibold {
+  font-weight: var(--font-weight-semibold) !important;
+}
+
+.font-bold {
+  font-weight: var(--font-weight-bold) !important;
+}
+
+/* 文本对齐 */
+.text-left {
+  text-align: left !important;
+}
+
+.text-center {
+  text-align: center !important;
+}
+
+.text-right {
+  text-align: right !important;
+}
+
+.text-justify {
+  text-align: justify !important;
+}
+
+/* ===== 紧凑布局工具类 ===== */
+.compact {
+  /* 紧凑间距 */
+  &-spacing {
+    > * + * {
+      margin-top: var(--spacing-2) !important;
+    }
+  }
+
+  /* 紧凑表单 */
+  &-form {
+    .el-form-item {
+      margin-bottom: var(--spacing-4) !important;
+    }
+
+    .el-input,
+    .el-select,
+    .el-textarea {
+      .el-input__inner,
+      .el-textarea__inner {
+        padding: var(--spacing-2) var(--spacing-3) !important;
+        font-size: var(--font-size-sm) !important;
+      }
+    }
+
+    .el-button {
+      padding: var(--spacing-2) var(--spacing-4) !important;
+      font-size: var(--font-size-sm) !important;
+    }
+  }
+
+  /* 紧凑表格 */
+  &-table {
+    .el-table {
+      th,
+      td {
+        padding: var(--spacing-2) var(--spacing-3) !important;
+        font-size: var(--font-size-sm) !important;
+      }
+    }
+  }
+
+  /* 紧凑按钮组 */
+  &-buttons {
+    display: flex;
+    gap: var(--spacing-2);
+
+    .el-button {
+      padding: var(--spacing-2) var(--spacing-3) !important;
+      font-size: var(--font-size-sm) !important;
+    }
+  }
+}
+
+/* ===== 间距工具类 ===== */
+/* 外边距 */
+.m-0 { margin: 0 !important; }
+.m-1 { margin: var(--spacing-1) !important; }
+.m-2 { margin: var(--spacing-2) !important; }
+.m-3 { margin: var(--spacing-3) !important; }
+.m-4 { margin: var(--spacing-4) !important; }
+.m-5 { margin: var(--spacing-5) !important; }
+.m-6 { margin: var(--spacing-6) !important; }
+.m-8 { margin: var(--spacing-8) !important; }
+
+/* 水平外边距 */
+.mx-0 { margin-left: 0 !important; margin-right: 0 !important; }
+.mx-1 { margin-left: var(--spacing-1) !important; margin-right: var(--spacing-1) !important; }
+.mx-2 { margin-left: var(--spacing-2) !important; margin-right: var(--spacing-2) !important; }
+.mx-3 { margin-left: var(--spacing-3) !important; margin-right: var(--spacing-3) !important; }
+.mx-4 { margin-left: var(--spacing-4) !important; margin-right: var(--spacing-4) !important; }
+.mx-auto { margin-left: auto !important; margin-right: auto !important; }
+
+/* 垂直外边距 */
+.my-0 { margin-top: 0 !important; margin-bottom: 0 !important; }
+.my-1 { margin-top: var(--spacing-1) !important; margin-bottom: var(--spacing-1) !important; }
+.my-2 { margin-top: var(--spacing-2) !important; margin-bottom: var(--spacing-2) !important; }
+.my-3 { margin-top: var(--spacing-3) !important; margin-bottom: var(--spacing-3) !important; }
+.my-4 { margin-top: var(--spacing-4) !important; margin-bottom: var(--spacing-4) !important; }
+
+/* 内边距 */
+.p-0 { padding: 0 !important; }
+.p-1 { padding: var(--spacing-1) !important; }
+.p-2 { padding: var(--spacing-2) !important; }
+.p-3 { padding: var(--spacing-3) !important; }
+.p-4 { padding: var(--spacing-4) !important; }
+.p-5 { padding: var(--spacing-5) !important; }
+.p-6 { padding: var(--spacing-6) !important; }
+.p-8 { padding: var(--spacing-8) !important; }
+
+/* 水平内边距 */
+.px-0 { padding-left: 0 !important; padding-right: 0 !important; }
+.px-1 { padding-left: var(--spacing-1) !important; padding-right: var(--spacing-1) !important; }
+.px-2 { padding-left: var(--spacing-2) !important; padding-right: var(--spacing-2) !important; }
+.px-3 { padding-left: var(--spacing-3) !important; padding-right: var(--spacing-3) !important; }
+.px-4 { padding-left: var(--spacing-4) !important; padding-right: var(--spacing-4) !important; }
+
+/* 垂直内边距 */
+.py-0 { padding-top: 0 !important; padding-bottom: 0 !important; }
+.py-1 { padding-top: var(--spacing-1) !important; padding-bottom: var(--spacing-1) !important; }
+.py-2 { padding-top: var(--spacing-2) !important; padding-bottom: var(--spacing-2) !important; }
+.py-3 { padding-top: var(--spacing-3) !important; padding-bottom: var(--spacing-3) !important; }
+.py-4 { padding-top: var(--spacing-4) !important; padding-bottom: var(--spacing-4) !important; }
+
+/* ===== 布局工具类 ===== */
+.flex {
+  display: flex !important;
+}
+
+.inline-flex {
+  display: inline-flex !important;
+}
+
+.flex-col {
+  flex-direction: column !important;
+}
+
+.flex-row {
+  flex-direction: row !important;
+}
+
+.flex-wrap {
+  flex-wrap: wrap !important;
+}
+
+.flex-nowrap {
+  flex-wrap: nowrap !important;
+}
+
+.items-start {
+  align-items: flex-start !important;
+}
+
+.items-center {
+  align-items: center !important;
+}
+
+.items-end {
+  align-items: flex-end !important;
+}
+
+.items-stretch {
+  align-items: stretch !important;
+}
+
+.justify-start {
+  justify-content: flex-start !important;
+}
+
+.justify-center {
+  justify-content: center !important;
+}
+
+.justify-end {
+  justify-content: flex-end !important;
+}
+
+.justify-between {
+  justify-content: space-between !important;
+}
+
+.justify-around {
+  justify-content: space-around !important;
+}
+
+.justify-evenly {
+  justify-content: space-evenly !important;
+}
+
+.flex-1 {
+  flex: 1 1 0% !important;
+}
+
+.flex-auto {
+  flex: 1 1 auto !important;
+}
+
+.flex-none {
+  flex: none !important;
+}
+
+.grow {
+  flex-grow: 1 !important;
+}
+
+.grow-0 {
+  flex-grow: 0 !important;
+}
+
+.shrink {
+  flex-shrink: 1 !important;
+}
+
+.shrink-0 {
+  flex-shrink: 0 !important;
+}
+
+/* ===== 显示工具类 ===== */
+.block {
+  display: block !important;
+}
+
+.inline-block {
+  display: inline-block !important;
+}
+
+.inline {
+  display: inline !important;
+}
+
+.hidden {
+  display: none !important;
+}
+
+.invisible {
+  visibility: hidden !important;
+}
+
+.visible {
+  visibility: visible !important;
+}
+
+/* ===== 位置工具类 ===== */
+.relative {
+  position: relative !important;
+}
+
+.absolute {
+  position: absolute !important;
+}
+
+.fixed {
+  position: fixed !important;
+}
+
+.sticky {
+  position: sticky !important;
+}
+
+.static {
+  position: static !important;
+}
+
+/* ===== 圆角工具类 ===== */
+.rounded-none {
+  border-radius: var(--radius-none) !important;
+}
+
+.rounded-sm {
+  border-radius: var(--radius-sm) !important;
+}
+
+.rounded {
+  border-radius: var(--radius-base) !important;
+}
+
+.rounded-md {
+  border-radius: var(--radius-md) !important;
+}
+
+.rounded-lg {
+  border-radius: var(--radius-lg) !important;
+}
+
+.rounded-xl {
+  border-radius: var(--radius-xl) !important;
+}
+
+.rounded-2xl {
+  border-radius: var(--radius-2xl) !important;
+}
+
+.rounded-full {
+  border-radius: var(--radius-full) !important;
+}
+
+/* ===== 阴影工具类 ===== */
+.shadow-none {
+  box-shadow: var(--shadow-none) !important;
+}
+
+.shadow-sm {
+  box-shadow: var(--shadow-sm) !important;
+}
+
+.shadow {
+  box-shadow: var(--shadow-base) !important;
+}
+
+.shadow-md {
+  box-shadow: var(--shadow-md) !important;
+}
+
+.shadow-lg {
+  box-shadow: var(--shadow-lg) !important;
+}
+
+.shadow-xl {
+  box-shadow: var(--shadow-xl) !important;
+}
+
+.shadow-2xl {
+  box-shadow: var(--shadow-2xl) !important;
+}
+
+.shadow-card {
+  box-shadow: var(--shadow-card) !important;
+}
+
+/* ===== 边框工具类 ===== */
+.border {
+  border: 1px solid var(--color-border-primary) !important;
+}
+
+.border-0 {
+  border: 0 !important;
+}
+
+.border-t {
+  border-top: 1px solid var(--color-border-primary) !important;
+}
+
+.border-r {
+  border-right: 1px solid var(--color-border-primary) !important;
+}
+
+.border-b {
+  border-bottom: 1px solid var(--color-border-primary) !important;
+}
+
+.border-l {
+  border-left: 1px solid var(--color-border-primary) !important;
+}
+
+.border-primary {
+  border-color: var(--color-primary) !important;
+}
+
+.border-success {
+  border-color: var(--color-success) !important;
+}
+
+.border-warning {
+  border-color: var(--color-warning) !important;
+}
+
+.border-danger {
+  border-color: var(--color-danger) !important;
+}
+
+/* ===== 背景工具类 ===== */
+.bg-transparent {
+  background-color: transparent !important;
+}
+
+.bg-primary {
+  background-color: var(--color-bg-primary) !important;
+}
+
+.bg-secondary {
+  background-color: var(--color-bg-secondary) !important;
+}
+
+.bg-tertiary {
+  background-color: var(--color-bg-tertiary) !important;
+}
+
+.bg-card {
+  background-color: var(--color-bg-card) !important;
+}
+
+.bg-success {
+  background-color: var(--color-success) !important;
+  color: var(--color-text-inverse) !important;
+}
+
+.bg-warning {
+  background-color: var(--color-warning) !important;
+  color: var(--color-text-inverse) !important;
+}
+
+.bg-danger {
+  background-color: var(--color-danger) !important;
+  color: var(--color-text-inverse) !important;
+}
+
+.bg-info {
+  background-color: var(--color-primary) !important;
+  color: var(--color-text-inverse) !important;
+}
+
+/* ===== 宽高工具类 ===== */
+.w-full {
+  width: 100% !important;
+}
+
+.w-auto {
+  width: auto !important;
+}
+
+.h-full {
+  height: 100% !important;
+}
+
+.h-auto {
+  height: auto !important;
+}
+
+.min-h-0 {
+  min-height: 0 !important;
+}
+
+.min-h-full {
+  min-height: 100% !important;
+}
+
+/* ===== 溢出工具类 ===== */
+.overflow-hidden {
+  overflow: hidden !important;
+}
+
+.overflow-visible {
+  overflow: visible !important;
+}
+
+.overflow-scroll {
+  overflow: scroll !important;
+}
+
+.overflow-auto {
+  overflow: auto !important;
+}
+
+.overflow-x-hidden {
+  overflow-x: hidden !important;
+}
+
+.overflow-y-hidden {
+  overflow-y: hidden !important;
+}
+
+.overflow-x-scroll {
+  overflow-x: scroll !important;
+}
+
+.overflow-y-scroll {
+  overflow-y: scroll !important;
+}
+
+.overflow-x-auto {
+  overflow-x: auto !important;
+}
+
+.overflow-y-auto {
+  overflow-y: auto !important;
+}
+
+/* ===== 光标工具类 ===== */
+.cursor-auto {
+  cursor: auto !important;
+}
+
+.cursor-default {
+  cursor: default !important;
+}
+
+.cursor-pointer {
+  cursor: pointer !important;
+}
+
+.cursor-wait {
+  cursor: wait !important;
+}
+
+.cursor-text {
+  cursor: text !important;
+}
+
+.cursor-move {
+  cursor: move !important;
+}
+
+.cursor-help {
+  cursor: help !important;
+}
+
+.cursor-not-allowed {
+  cursor: not-allowed !important;
+}
+
+/* ===== 用户选择工具类 ===== */
+.select-none {
+  user-select: none !important;
+}
+
+.select-text {
+  user-select: text !important;
+}
+
+.select-all {
+  user-select: all !important;
+}
+
+.select-auto {
+  user-select: auto !important;
+}
+
+/* ===== 过渡动画工具类 ===== */
+.transition {
+  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
+  transition-timing-function: var(--ease-in-out);
+  transition-duration: var(--duration-150);
+}
+
+.transition-none {
+  transition-property: none !important;
+}
+
+.transition-all {
+  transition-property: all !important;
+  transition-timing-function: var(--ease-in-out);
+  transition-duration: var(--duration-150);
+}
+
+.transition-colors {
+  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+  transition-timing-function: var(--ease-in-out);
+  transition-duration: var(--duration-150);
+}
+
+.transition-opacity {
+  transition-property: opacity;
+  transition-timing-function: var(--ease-in-out);
+  transition-duration: var(--duration-150);
+}
+
+.transition-shadow {
+  transition-property: box-shadow;
+  transition-timing-function: var(--ease-in-out);
+  transition-duration: var(--duration-150);
+}
+
+.transition-transform {
+  transition-property: transform;
+  transition-timing-function: var(--ease-in-out);
+  transition-duration: var(--duration-150);
+}
+
+/* 过渡时长 */
+.duration-75 {
+  transition-duration: var(--duration-75) !important;
+}
+
+.duration-100 {
+  transition-duration: var(--duration-100) !important;
+}
+
+.duration-150 {
+  transition-duration: var(--duration-150) !important;
+}
+
+.duration-200 {
+  transition-duration: var(--duration-200) !important;
+}
+
+.duration-300 {
+  transition-duration: var(--duration-300) !important;
+}
+
+.duration-500 {
+  transition-duration: var(--duration-500) !important;
+}
+
+/* 过渡缓动 */
+.ease-linear {
+  transition-timing-function: var(--ease-linear) !important;
+}
+
+.ease-in {
+  transition-timing-function: var(--ease-in) !important;
+}
+
+.ease-out {
+  transition-timing-function: var(--ease-out) !important;
+}
+
+.ease-in-out {
+  transition-timing-function: var(--ease-in-out) !important;
+}
+
+/* ===== 响应式工具类 ===== */
+@media (max-width: 639px) {
+  .sm\:hidden {
+    display: none !important;
+  }
+  
+  .sm\:block {
+    display: block !important;
+  }
+  
+  .sm\:flex {
+    display: flex !important;
+  }
+  
+  .sm\:text-sm {
+    font-size: var(--font-size-sm) !important;
+  }
+  
+  .sm\:p-4 {
+    padding: var(--spacing-4) !important;
+  }
+}
+
+@media (min-width: 640px) and (max-width: 767px) {
+  .md\:hidden {
+    display: none !important;
+  }
+  
+  .md\:block {
+    display: block !important;
+  }
+  
+  .md\:flex {
+    display: flex !important;
+  }
+}
+
+@media (min-width: 768px) {
+  .lg\:hidden {
+    display: none !important;
+  }
+  
+  .lg\:block {
+    display: block !important;
+  }
+  
+  .lg\:flex {
+    display: flex !important;
+  }
+  
+  .lg\:text-base {
+    font-size: var(--font-size-base) !important;
+  }
+  
+  .lg\:p-6 {
+    padding: var(--spacing-6) !important;
+  }
+}
+
+/* ===== 打印工具类 ===== */
+@media print {
+  .print\:hidden {
+    display: none !important;
+  }
+  
+  .print\:block {
+    display: block !important;
+  }
+  
+  .print\:text-black {
+    color: #000000 !important;
+  }
+  
+  .print\:bg-white {
+    background-color: #ffffff !important;
+  }
+}
+
+/* ===== 可访问性工具类 ===== */
+.sr-only {
+  position: absolute !important;
+  width: 1px !important;
+  height: 1px !important;
+  padding: 0 !important;
+  margin: -1px !important;
+  overflow: hidden !important;
+  clip: rect(0, 0, 0, 0) !important;
+  white-space: nowrap !important;
+  border: 0 !important;
+}
+
+.not-sr-only {
+  position: static !important;
+  width: auto !important;
+  height: auto !important;
+  padding: 0 !important;
+  margin: 0 !important;
+  overflow: visible !important;
+  clip: auto !important;
+  white-space: normal !important;
+}
+
+/* 聚焦可见性 */
+.focus\:outline-none:focus {
+  outline: 2px solid transparent !important;
+  outline-offset: 2px !important;
+}
+
+.focus\:ring:focus {
+  outline: 2px solid transparent !important;
+  outline-offset: 2px !important;
+  box-shadow: 0 0 0 3px var(--color-primary-100) !important;
+}
+
+.focus\:ring-primary:focus {
+  box-shadow: 0 0 0 3px var(--color-primary-100) !important;
+}
+
+.focus\:ring-success:focus {
+  box-shadow: 0 0 0 3px var(--color-success-100) !important;
+}
+
+.focus\:ring-warning:focus {
+  box-shadow: 0 0 0 3px var(--color-warning-100) !important;
+}
+
+.focus\:ring-danger:focus {
+  box-shadow: 0 0 0 3px var(--color-danger-100) !important;
+}

+ 229 - 0
src/utils/map-operations.js

@@ -0,0 +1,229 @@
+/**
+ * 地图操作工具类
+ * 提供通用的地图操作方法,供导航页和标定页共同使用
+ */
+
+export class MapOperations {
+  constructor(mapRef) {
+    this.mapRef = mapRef
+  }
+
+  /**
+   * 放大地图
+   */
+  zoomIn() {
+    if (this.mapRef && this.mapRef.zoomIn) {
+      this.mapRef.zoomIn()
+      return true
+    }
+    return false
+  }
+
+  /**
+   * 缩小地图
+   */
+  zoomOut() {
+    if (this.mapRef && this.mapRef.zoomOut) {
+      this.mapRef.zoomOut()
+      return true
+    }
+    return false
+  }
+
+  /**
+   * 居中到机器人位置
+   * @param {Object} robotPosition - 机器人位置信息 {x, y} 或 经纬度
+   */
+  centerToRobot(robotPosition = null) {
+    if (!this.mapRef) {
+      return false
+    }
+
+    // 如果传入了位置参数,使用传入的位置
+    if (robotPosition && (robotPosition.x !== undefined || robotPosition.lng !== undefined)) {
+      if (this.mapRef.centerToPosition) {
+        this.mapRef.centerToPosition(robotPosition)
+        return true
+      }
+    }
+
+    // 否则尝试调用地图组件的centerToRobot方法
+    if (this.mapRef.centerToRobot) {
+      this.mapRef.centerToRobot()
+      return true
+    }
+
+    return false
+  }
+
+  /**
+   * 适配地图到画布
+   */
+  fitToCanvas() {
+    if (this.mapRef && this.mapRef.fitToCanvas) {
+      this.mapRef.fitToCanvas()
+      return true
+    }
+    return false
+  }
+
+  /**
+   * 更新地图尺寸
+   */
+  updateSize() {
+    if (this.mapRef && this.mapRef.updateSize) {
+      this.mapRef.updateSize()
+      return true
+    }
+    return false
+  }
+
+  /**
+   * 检查地图是否已就绪
+   */
+  isMapReady() {
+    return !!(this.mapRef && this.mapRef.getMap)
+  }
+
+  /**
+   * 获取地图实例
+   */
+  getMapInstance() {
+    if (this.mapRef && this.mapRef.getMap) {
+      return this.mapRef.getMap()
+    }
+    return null
+  }
+}
+
+/**
+ * 全屏操作工具类
+ */
+export class FullscreenOperations {
+  /**
+   * 切换全屏状态
+   * @param {HTMLElement} element - 要全屏的元素
+   * @param {Function} onStateChange - 状态变化回调
+   */
+  static toggleFullscreen(element, onStateChange = null) {
+    if (!element) return false
+
+    const isCurrentlyFullscreen = !!(
+      document.fullscreenElement || 
+      document.webkitFullscreenElement || 
+      document.mozFullScreenElement
+    )
+
+    if (!isCurrentlyFullscreen) {
+      // 进入全屏
+      const requestFullscreen = element.requestFullscreen || 
+        element.webkitRequestFullscreen || 
+        element.mozRequestFullScreen
+
+      if (requestFullscreen) {
+        requestFullscreen.call(element)
+        return true
+      }
+    } else {
+      // 退出全屏
+      const exitFullscreen = document.exitFullscreen || 
+        document.webkitExitFullscreen || 
+        document.mozCancelFullScreen
+
+      if (exitFullscreen) {
+        exitFullscreen.call(document)
+        return true
+      }
+    }
+
+    return false
+  }
+
+  /**
+   * 添加全屏状态监听器
+   * @param {Function} callback - 状态变化回调
+   */
+  static addFullscreenListener(callback) {
+    const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange']
+    const handler = () => {
+      const isFullscreen = !!(
+        document.fullscreenElement || 
+        document.webkitFullscreenElement || 
+        document.mozFullScreenElement
+      )
+      callback(isFullscreen)
+    }
+
+    events.forEach(event => {
+      document.addEventListener(event, handler)
+    })
+
+    // 返回清理函数
+    return () => {
+      events.forEach(event => {
+        document.removeEventListener(event, handler)
+      })
+    }
+  }
+}
+
+/**
+ * 机器人位置工具类
+ */
+export class RobotPositionUtils {
+  /**
+   * 解析坐标字符串
+   * @param {String} coordinates - 坐标字符串,如 "(1.813, -63.931, 0.000)"
+   * @returns {Object|null} - {x, y, z} 或 null
+   */
+  static parseCoordinates(coordinates) {
+    if (!coordinates || typeof coordinates !== 'string') {
+      return null
+    }
+
+    // 匹配形如 "(x, y, z)" 的字符串
+    const match = coordinates.match(/\(([^,]+),\s*([^,]+),\s*([^)]+)\)/)
+    if (!match) {
+      return null
+    }
+
+    const x = parseFloat(match[1])
+    const y = parseFloat(match[2])
+    const z = parseFloat(match[3])
+
+    if (isNaN(x) || isNaN(y) || isNaN(z)) {
+      return null
+    }
+
+    return { x, y, z }
+  }
+
+  /**
+   * 检查机器人位置是否有效
+   * @param {Object} position - 位置对象
+   * @returns {Boolean}
+   */
+  static isValidPosition(position) {
+    if (!position) return false
+    
+    // 检查是否为零点或无效值
+    if (position.x === 0 && position.y === 0) return false
+    if (isNaN(position.x) || isNaN(position.y)) return false
+    
+    return true
+  }
+
+  /**
+   * 从实时信息中获取机器人位置
+   * @param {Object} realtimeInfo - 实时信息对象
+   * @returns {Object|null}
+   */
+  static getRobotPosition(realtimeInfo) {
+    if (!realtimeInfo || !realtimeInfo.coordinates) {
+      return null
+    }
+
+    const position = this.parseCoordinates(realtimeInfo.coordinates)
+    return this.isValidPosition(position) ? position : null
+  }
+}

+ 411 - 0
src/utils/websocket.js

@@ -0,0 +1,411 @@
+/**
+ * WebSocket服务 - 替代MQTT直接连接
+ * 通过WebSocket订阅机器人实时数据(位姿、轨迹、任务状态等)
+ * 后端通过STOMP协议推送MQTT接收到的机器人数据
+ */
+import { ElNotification, ElMessage } from 'element-plus'
+import SockJS from 'sockjs-client'
+import Stomp from 'stompjs'
+
+// 创建STOMP客户端
+let stompClient = null
+let socket = null
+let isConnected = false
+let reconnectTimer = null
+let reconnectAttempts = 0
+const MAX_RECONNECT_ATTEMPTS = 10
+const RECONNECT_INTERVAL = 5000
+
+// 订阅回调函数存储
+const subscriptions = {}
+
+// WebSocket连接配置
+let wsConfig = {
+  baseUrl: '',  // 会被设置为 {wsBase}/ws/robot
+  deviceId: 'ld000001',
+  onConnect: null,
+  onDisconnect: null,
+  onMessage: null,
+  onError: null
+}
+
+/**
+ * 初始化WebSocket连接
+ * @param {Object} config 配置对象
+ * @param {string} config.baseUrl WebSocket基础URL
+ * @param {string} config.deviceId 设备ID
+ * @param {Function} config.onConnect 连接成功回调
+ * @param {Function} config.onDisconnect 断开连接回调
+ * @param {Function} config.onMessage 消息接收回调
+ * @param {Function} config.onError 错误回调
+ */
+export function initWebSocket(config) {
+  console.log('[WebSocket] initWebSocket 被调用,传入参数:', config)
+  console.log('[WebSocket] 当前 wsConfig:', JSON.parse(JSON.stringify(wsConfig)))
+  
+  // 合并配置:如果 config 中没有 deviceId,则保留 wsConfig 的默认值
+  const mergedConfig = { ...wsConfig }
+  if (config) {
+    Object.keys(config).forEach(key => {
+      if (config[key] !== undefined && config[key] !== null) {
+        mergedConfig[key] = config[key]
+      }
+    })
+  }
+  wsConfig = mergedConfig
+  
+  console.log('[WebSocket] 合并后 wsConfig:', JSON.parse(JSON.stringify(wsConfig)))
+  
+  connect()
+}
+
+/**
+ * 建立WebSocket连接
+ */
+function connect() {
+  try {
+    // 获取WebSocket URL
+    // 如果配置了baseUrl,直接使用;否则从当前域名推断
+    let wsUrl = wsConfig.baseUrl
+    if (!wsUrl) {
+      const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:'
+      const host = window.location.host
+      wsUrl = `${protocol}//${host}`
+    }
+    
+    const socketUrl = `${wsUrl}/ws/robot`
+    
+    console.log('[WebSocket] ===== WebSocket连接调试 =====')
+    console.log('[WebSocket] 当前页面:', window.location.href)
+    console.log('[WebSocket] 协议:', window.location.protocol)
+    console.log('[WebSocket] 主机:', window.location.host)
+    console.log('[WebSocket] 目标URL:', socketUrl)
+    console.log('[WebSocket] 设备ID:', wsConfig.deviceId)
+    console.log('[WebSocket] ==============================')
+    
+    // 创建SockJS连接
+    socket = new SockJS(socketUrl)
+    
+    // 创建STOMP客户端
+    stompClient = Stomp.over(socket)
+    
+    // 配置STOMP参数
+    stompClient.connectHeaders = {}
+    
+    // 添加连接超时检测
+    const connectionTimeout = setTimeout(() => {
+      console.error('[WebSocket] 连接超时(10秒),可能连接失败')
+    }, 10000)
+    stompClient.debug = function(str) {
+      if (str.includes('CONNECTED')) {
+        console.log('[WebSocket] ✅ STOMP连接成功')
+      } else if (str.includes('ERROR')) {
+        console.error('[WebSocket] ❌ STOMP错误:', str)
+      } else {
+        console.log('[WebSocket]', str)
+      }
+    }
+    
+    // 连接成功回调
+    stompClient.connect({}, 
+      function(frame) {
+        clearTimeout(connectionTimeout)
+        isConnected = true
+        reconnectAttempts = 0
+        console.log('[WebSocket] 已连接:', frame)
+        
+        // 触发连接成功回调
+        if (wsConfig.onConnect) {
+          wsConfig.onConnect()
+        }
+        
+        // 自动订阅设备专属频道
+        if (wsConfig.deviceId) {
+          subscribeToDevice(wsConfig.deviceId)
+        }
+        
+        ElNotification({
+          title: 'WebSocket连接成功',
+          message: '已连接到机器人实时数据服务',
+          type: 'success',
+          duration: 3000
+        })
+      },
+      function(error) {
+        console.error('[WebSocket] 连接错误:', error)
+        console.error('[WebSocket] 错误类型:', typeof error)
+        console.error('[WebSocket] 错误详情:', JSON.stringify(error))
+        isConnected = false
+        
+        if (wsConfig.onError) {
+          wsConfig.onError(error)
+        }
+        
+        // 触发断开连接回调
+        if (wsConfig.onDisconnect) {
+          wsConfig.onDisconnect()
+        }
+        
+        // 自动重连
+        handleReconnect()
+      }
+    )
+  } catch (error) {
+    console.error('[WebSocket] 连接异常:', error)
+    handleReconnect()
+  }
+}
+
+/**
+ * 处理自动重连
+ */
+function handleReconnect() {
+  if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
+    console.error('[WebSocket] 达到最大重连次数,停止重连')
+    ElMessage.error('WebSocket连接失败,请刷新页面或检查网络')
+    return
+  }
+  
+  reconnectAttempts++
+  console.log(`[WebSocket] ${RECONNECT_INTERVAL/1000}秒后尝试第${reconnectAttempts}次重连...`)
+  
+  if (reconnectTimer) {
+    clearTimeout(reconnectTimer)
+  }
+  
+  reconnectTimer = setTimeout(() => {
+    console.log(`[WebSocket] 正在进行第${reconnectAttempts}次重连...`)
+    connect()
+  }, RECONNECT_INTERVAL)
+}
+
+/**
+ * 订阅设备专属频道
+ * @param {string} deviceId 设备ID
+ */
+export function subscribeToDevice(deviceId) {
+  if (!stompClient || !isConnected) {
+    console.warn('[WebSocket] 未连接,无法订阅')
+    return
+  }
+  
+  wsConfig.deviceId = deviceId
+  
+  // 取消之前的订阅
+  unsubscribeAll()
+  
+  const prefix = `/topic/robot/${deviceId}`
+  
+  // 订阅位姿数据
+  subscribe(`${prefix}/pose`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('pose', message)
+    }
+  })
+  
+  // 订阅任务实时信息
+  subscribe(`${prefix}/task`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('task', message)
+    }
+  })
+  
+  // 订阅轨迹数据
+  subscribe(`${prefix}/trajectory`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('trajectory', message)
+    }
+  })
+  
+  // 订阅到达事件
+  subscribe(`${prefix}/event`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('arrive', message)
+    }
+  })
+  
+  // 订阅地图数据
+  subscribe(`${prefix}/map`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('map', message)
+    }
+  })
+  
+  // 订阅导航响应
+  subscribe(`${prefix}/navigation`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('navigation', message)
+    }
+  })
+  
+  // 订阅规划响应
+  subscribe(`${prefix}/planning`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('planning', message)
+    }
+  })
+  
+  // 订阅原始MQTT消息(用于调试)
+  subscribe(`${prefix}/raw`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('raw', message)
+    }
+  })
+  
+  // 订阅ASM功能状态(用于录制/构建/SLAM)
+  subscribe(`${prefix}/asm`, (message) => {
+    if (wsConfig.onMessage) {
+      wsConfig.onMessage('asm', message)
+    }
+  })
+  
+  console.log(`[WebSocket] 已订阅设备 ${deviceId} 的所有频道`)
+}
+
+/**
+ * 订阅指定频道
+ * @param {string} channel 频道路径
+ * @param {Function} callback 回调函数
+ */
+export function subscribe(channel, callback) {
+  if (!stompClient || !isConnected) {
+    console.warn(`[WebSocket] 未连接,无法订阅频道: ${channel}`)
+    return
+  }
+  
+  const subscription = stompClient.subscribe(channel, (message) => {
+    try {
+      const body = JSON.parse(message.body)
+      callback(body)
+    } catch (e) {
+      console.error(`[WebSocket] 解析消息失败:`, e, message.body)
+    }
+  })
+  
+  subscriptions[channel] = subscription
+  console.log(`[WebSocket] 已订阅频道: ${channel}`)
+  
+  return subscription
+}
+
+/**
+ * 取消订阅指定频道
+ * @param {string} channel 频道路径
+ */
+export function unsubscribe(channel) {
+  if (subscriptions[channel]) {
+    subscriptions[channel].unsubscribe()
+    delete subscriptions[channel]
+    console.log(`[WebSocket] 已取消订阅: ${channel}`)
+  }
+}
+
+/**
+ * 取消所有订阅
+ */
+export function unsubscribeAll() {
+  Object.keys(subscriptions).forEach(channel => {
+    if (subscriptions[channel]) {
+      subscriptions[channel].unsubscribe()
+    }
+  })
+  Object.keys(subscriptions).forEach(key => delete subscriptions[key])
+  console.log('[WebSocket] 已取消所有订阅')
+}
+
+/**
+ * 断开WebSocket连接
+ */
+export function disconnect() {
+  if (reconnectTimer) {
+    clearTimeout(reconnectTimer)
+    reconnectTimer = null
+  }
+  
+  unsubscribeAll()
+  
+  if (stompClient) {
+    stompClient.disconnect(() => {
+      console.log('[WebSocket] 已断开连接')
+    })
+    stompClient = null
+  }
+  
+  if (socket) {
+    socket.close()
+    socket = null
+  }
+  
+  isConnected = false
+  // 重置配置时保留 deviceId 的默认值
+  wsConfig = {
+    baseUrl: '',
+    deviceId: 'ld000001',  // 保持默认值,不重置为空
+    onConnect: null,
+    onDisconnect: null,
+    onMessage: null,
+    onError: null
+  }
+}
+
+/**
+ * 发送STOMP消息
+ * @param {string} destination 目标路径
+ * @param {Object} data 发送的数据
+ */
+export function sendMessage(destination, data) {
+  if (!stompClient || !isConnected) {
+    console.warn('[WebSocket] 未连接,无法发送消息')
+    return false
+  }
+  
+  try {
+    stompClient.send(destination, {}, JSON.stringify(data))
+    console.log(`[WebSocket] 已发送消息到 ${destination}:`, data)
+    return true
+  } catch (error) {
+    console.error('[WebSocket] 发送消息失败:', error)
+    return false
+  }
+}
+
+/**
+ * 获取连接状态
+ */
+export function getConnectionStatus() {
+  return {
+    isConnected,
+    reconnectAttempts,
+    subscriptions: Object.keys(subscriptions)
+  }
+}
+
+/**
+ * 重新连接
+ */
+export function reconnect() {
+  disconnect()
+  reconnectAttempts = 0
+  connect()
+}
+
+/**
+ * 切换设备订阅
+ * @param {string} newDeviceId 新的设备ID
+ */
+export function switchDevice(newDeviceId) {
+  if (newDeviceId === wsConfig.deviceId) {
+    console.log('[WebSocket] 设备ID未变化,无需切换')
+    return
+  }
+  
+  console.log(`[WebSocket] 切换设备: ${wsConfig.deviceId} -> ${newDeviceId}`)
+  wsConfig.deviceId = newDeviceId
+  
+  if (isConnected) {
+    subscribeToDevice(newDeviceId)
+  }
+}
+
+// 导出STOMP和SockJS供外部使用(如果需要自定义连接)
+export { stompClient, socket, SockJS, Stomp }

+ 652 - 0
src/views/map/components/shared/MapToolbar.vue

@@ -0,0 +1,652 @@
+<template>
+  <div class="map-toolbar" :style="toolbarStyle">
+    <!-- 内置工具或自定义工具 -->
+    <template v-if="preset !== 'custom' || tools.length > 0">
+      <!-- 预设或自定义工具模式 -->
+      <div v-for="(tool, index) in visibleTools" :key="tool.key" class="toolbar-item-wrapper">
+        <!-- 按钮类型 -->
+        <div v-if="tool.type === 'btn'" class="toolbar-item">
+          <el-tooltip
+            :content="getToolTooltip(tool)"
+            :placement="vertical ? 'right' : 'bottom'"
+          >
+            <el-button
+              :size="buttonSize"
+              :type="tool.buttonType || 'default'"
+              circle
+              :disabled="tool.disabled || isToolDisabled(tool.key)"
+              :class="[
+                'toolbar-btn',
+                {
+                  'primary-btn': tool.buttonType === 'primary',
+                  'active': selectedKey === tool.key,
+                  'has-custom-icon': tool.customIcon
+                }
+              ]"
+              @click="handleToolClick(tool)"
+            >
+              <!-- Element Plus 图标 -->
+              <template v-if="!tool.customIcon && tool.icon">
+                <el-icon>
+                  <component :is="getIconComponent(tool.icon)" />
+                </el-icon>
+              </template>
+
+              <!-- 自定义图标 -->
+              <template v-else-if="tool.customIcon">
+                <!-- 选择工具图标 -->
+                <svg v-if="tool.customIcon === 'select'" viewBox="0 0 24 24" class="custom-icon">
+                  <g transform="scale(0.024)">
+                    <path d="M439.04 141.6192c17.5104 2.5088 29.6448 18.7392 27.136 36.1984l-12.2368 85.504a32 32 0 0 1-63.3344-9.0112l12.2368-85.504a32 32 0 0 1 36.1984-27.136zM783.36 163.84h-194.56a30.72 30.72 0 0 0 0 61.44h194.56q10.24 0 10.24 10.24v204.8a30.72 30.72 0 1 0 61.44 0v-204.8q0-29.696-20.992-50.688-20.992-20.992-50.688-20.992zM326.7072 252.672a32 32 0 0 1-50.5856 39.2192L221.184 220.9792a32 32 0 0 1 50.5856-39.2192L326.656 252.672z m-72.3456 95.232a32 32 0 0 1-9.0624 63.3856L159.744 399.0528a32 32 0 0 1 9.0624-63.3344l85.504 12.2368z m607.6416 217.856L422.8096 372.5312c-19.0464-8.2432-36.608 2.56-29.3888 24.32l157.184 472.7808c5.7344 17.5616 16.896 18.5344 24.9856 2.1504q40.192-83.2 75.9296-138.3424a10.0352 10.0352 0 0 1 16.0768-0.9728l98.9184 114.5344a26.7264 26.7264 0 0 0 38.912 2.2528l26.6752-25.4464a30.208 30.208 0 0 0 1.536-40.96l-99.6864-115.456a10.0864 10.0864 0 0 1 2.5088-15.36q52.48-30.6176 125.5424-60.8256c16.384-6.7584 16.384-18.2272 0-25.4464zM235.52 870.4H409.6a30.72 30.72 0 0 0 0-61.44H235.52q-10.24 0-10.24-10.24v-276.48a30.72 30.72 0 0 0-61.44 0v276.48q0 29.696 20.992 50.688 20.992 20.992 50.688 20.992z"
+                          fill="currentColor"/>
+                  </g>
+                </svg>
+
+                <!-- 绘制点图标 -->
+                <svg v-else-if="tool.customIcon === 'point'" viewBox="0 0 24 24" class="custom-icon">
+                  <circle cx="12" cy="12" r="4" fill="currentColor"/>
+                  <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-dasharray="2,2" opacity="0.6"/>
+                </svg>
+
+                <!-- 绘制线图标 -->
+                <svg v-else-if="tool.customIcon === 'line'" viewBox="0 0 24 24" class="custom-icon">
+                  <path d="M4 20L20 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+                  <circle cx="4" cy="20" r="2" fill="currentColor"/>
+                  <circle cx="20" cy="4" r="2" fill="currentColor"/>
+                </svg>
+
+                <!-- 绘制曲线图标 -->
+                <svg v-else-if="tool.customIcon === 'curve'" viewBox="0 0 24 24" class="custom-icon">
+                  <path d="M3 20C3 20 7 4 12 12C17 20 21 4 21 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>
+                  <circle cx="3" cy="20" r="2" fill="currentColor"/>
+                  <circle cx="21" cy="4" r="2" fill="currentColor"/>
+                </svg>
+
+                <!-- 绘制区域图标 -->
+                <svg v-else-if="tool.customIcon === 'polygon'" viewBox="0 0 24 24" class="custom-icon">
+                  <path d="M12 2L22 8.5L18 20H6L2 8.5L12 2Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
+                  <circle cx="12" cy="2" r="1.5" fill="currentColor"/>
+                  <circle cx="22" cy="8.5" r="1.5" fill="currentColor"/>
+                  <circle cx="18" cy="20" r="1.5" fill="currentColor"/>
+                  <circle cx="6" cy="20" r="1.5" fill="currentColor"/>
+                  <circle cx="2" cy="8.5" r="1.5" fill="currentColor"/>
+                </svg>
+              </template>
+            </el-button>
+          </el-tooltip>
+        </div>
+        <!-- 分隔线类型 -->
+        <div
+          v-else-if="tool.type === 'divider'"
+          class="toolbar-divider"
+          aria-hidden="true"
+        ></div>
+      </div>
+    </template>
+
+    <template v-else>
+      <!-- 默认内置工具模式(兼容原有标定页) -->
+      <el-tooltip content="放大" :placement="vertical ? 'right' : 'bottom'">
+        <el-button
+          :size="buttonSize"
+          circle
+          @click="zoomIn"
+          class="toolbar-btn"
+        >
+          <el-icon><Plus /></el-icon>
+        </el-button>
+      </el-tooltip>
+
+      <el-tooltip content="缩小" :placement="vertical ? 'right' : 'bottom'">
+        <el-button
+          :size="buttonSize"
+          circle
+          @click="zoomOut"
+          class="toolbar-btn"
+        >
+          <el-icon><Minus /></el-icon>
+        </el-button>
+      </el-tooltip>
+
+      <el-tooltip content="居中到机器人" :placement="vertical ? 'right' : 'bottom'">
+        <el-button
+          :size="buttonSize"
+          circle
+          @click="centerToRobot"
+          :disabled="!hasRobotPosition"
+          class="toolbar-btn"
+        >
+          <el-icon><LocationFilled /></el-icon>
+        </el-button>
+      </el-tooltip>
+
+      <el-tooltip :content="isFullscreen ? '退出全屏' : '全屏'" :placement="vertical ? 'right' : 'bottom'">
+        <el-button
+          :size="buttonSize"
+          circle
+          @click="toggleFullscreen"
+          class="toolbar-btn"
+        >
+          <el-icon><FullScreen /></el-icon>
+        </el-button>
+      </el-tooltip>
+
+      <!-- 分割线 -->
+      <div class="toolbar-divider"></div>
+
+      <!-- 主要操作按钮 -->
+      <el-tooltip content="添加标定点" :placement="vertical ? 'right' : 'bottom'">
+        <el-button
+          type="primary"
+          :size="buttonSize"
+          circle
+          @click="addCalibrationPoint"
+          class="toolbar-btn primary-btn"
+          :disabled="!canAddCalibration"
+        >
+          <el-icon><CirclePlusFilled /></el-icon>
+        </el-button>
+      </el-tooltip>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { ElMessageBox, ElIcon } from 'element-plus'
+import {
+  Plus,
+  Minus,
+  LocationFilled,
+  FullScreen,
+  CirclePlusFilled,
+  Refresh,
+  SwitchButton,
+  Position,
+  Aim,
+  Check
+} from '@element-plus/icons-vue'
+
+const props = defineProps({
+  // 预设配置
+  preset: {
+    type: String,
+    default: 'custom',
+    validator: value => ['custom', 'calibration', 'nav', 'edit'].includes(value)
+  },
+  // 自定义工具配置
+  tools: {
+    type: Array,
+    default: () => []
+  },
+  // 布局方向
+  vertical: {
+    type: Boolean,
+    default: true
+  },
+  // 按钮尺寸
+  size: {
+    type: String,
+    default: 'md',
+    validator: value => ['sm', 'md', 'lg'].includes(value)
+  },
+  // 当前选中的工具
+  selectedKey: {
+    type: String,
+    default: ''
+  },
+  // 禁用的工具列表
+  disabledKeys: {
+    type: Array,
+    default: () => []
+  },
+  // 位置偏移
+  offset: {
+    type: Object,
+    default: () => ({ top: '16px', left: '16px' })
+  },
+
+  // === 兼容原标定页的props ===
+  canAddCalibration: {
+    type: Boolean,
+    default: true
+  },
+  hasRobotPosition: {
+    type: Boolean,
+    default: false
+  },
+  isFullscreen: {
+    type: Boolean,
+    default: false
+  },
+  showAdvanced: {
+    type: Boolean,
+    default: false
+  },
+  // 连接状态
+  isConnected: {
+    type: Boolean,
+    default: true
+  },
+  // 系统忙碌状态
+  isBusy: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits([
+  'click',
+  'zoom-in',
+  'zoom-out',
+  'center-robot',
+  'center-to-robot',
+  'toggle-fullscreen',
+  'add-calibration-point',
+  'mode-change',
+  'save',
+  'reset-view',
+  'toggle-measure-tool',
+  'confirm-init',
+  'confirm-reboot',
+  'confirm-stop'
+])
+
+// 图标映射表
+const iconMap = {
+  'el-icon-plus': Plus,
+  'el-icon-minus': Minus,
+  'el-icon-location': LocationFilled,
+  'el-icon-full-screen': FullScreen,
+  'el-icon-aim': Aim,
+  'el-icon-circle-plus': CirclePlusFilled,
+  'el-icon-refresh': Refresh,
+  'el-icon-switch-button': SwitchButton,
+  'el-icon-position': Position,
+  'el-icon-check': Check
+}
+
+// 获取图标组件
+function getIconComponent(iconName) {
+  return iconMap[iconName] || iconName
+}
+
+// 当前使用的工具配置
+const currentTools = computed(() => {
+  if (props.preset === 'custom' && props.tools.length > 0) {
+    return props.tools
+  }
+  return getPresetTools()
+})
+
+// 可见的工具列表
+const visibleTools = computed(() => {
+  return currentTools.value.filter(tool => tool.visible !== false)
+})
+
+// 按钮尺寸映射
+const buttonSize = computed(() => {
+  const sizeMap = {
+    'sm': 'small',
+    'md': 'small',
+    'lg': 'default'
+  }
+  return sizeMap[props.size] || 'small'
+})
+
+// 工具栏样式
+const toolbarStyle = computed(() => {
+  return {
+    top: props.offset.top,
+    left: props.offset.left,
+    right: props.offset.right,
+    bottom: props.offset.bottom,
+    flexDirection: props.vertical ? 'column' : 'row'
+  }
+})
+
+// 获取预设工具配置
+function getPresetTools() {
+  const presets = {
+    calibration: [
+      { key: 'zoom-in', type: 'btn', icon: 'el-icon-plus', tooltip: '放大' },
+      { key: 'zoom-out', type: 'btn', icon: 'el-icon-minus', tooltip: '缩小' },
+      { key: 'center-robot', type: 'btn', icon: 'el-icon-location', tooltip: '居中到机器人' },
+      { key: 'toggle-fullscreen', type: 'btn', icon: props.isFullscreen ? 'el-icon-aim' : 'el-icon-full-screen', tooltip: props.isFullscreen ? '退出全屏' : '进入全屏' },
+      { key: 'divider-1', type: 'divider' },
+      { key: 'add-calibration', type: 'btn', icon: 'el-icon-circle-plus', tooltip: '添加标定点', buttonType: 'primary' }
+    ],
+    nav: [
+      { key: 'zoom-in', type: 'btn', icon: 'el-icon-plus', tooltip: '放大' },
+      { key: 'zoom-out', type: 'btn', icon: 'el-icon-minus', tooltip: '缩小' },
+      { key: 'center-robot', type: 'btn', icon: 'el-icon-location', tooltip: '居中到机器人' },
+      { key: 'toggle-fullscreen', type: 'btn', icon: props.isFullscreen ? 'el-icon-aim' : 'el-icon-full-screen', tooltip: props.isFullscreen ? '退出全屏' : '进入全屏' },
+      { key: 'divider-1', type: 'divider' },
+      { key: 'init-pose', type: 'btn', icon: 'el-icon-position', tooltip: '初始化' },
+      { key: 'reboot', type: 'btn', icon: 'el-icon-refresh', tooltip: '重启' },
+      { key: 'emergency-stop', type: 'btn', icon: 'el-icon-switch-button', tooltip: '结束/急停' }
+    ],
+    edit: [
+      { key: 'zoom-in', type: 'btn', icon: 'el-icon-plus', tooltip: '放大' },
+      { key: 'zoom-out', type: 'btn', icon: 'el-icon-minus', tooltip: '缩小' },
+      { key: 'center-robot', type: 'btn', icon: 'el-icon-location', tooltip: '居中到机器人' },
+      { key: 'toggle-fullscreen', type: 'btn', icon: props.isFullscreen ? 'el-icon-aim' : 'el-icon-full-screen', tooltip: props.isFullscreen ? '退出全屏' : '进入全屏' },
+      { key: 'divider-1', type: 'divider' },
+      { key: 'select-mode', type: 'btn', customIcon: 'select', tooltip: '元素选择' },
+      { key: 'draw-point', type: 'btn', customIcon: 'point', tooltip: '绘制点' },
+      { key: 'draw-line', type: 'btn', customIcon: 'line', tooltip: '绘制线' },
+      { key: 'draw-curve', type: 'btn', customIcon: 'curve', tooltip: '绘制曲线' },
+      { key: 'draw-polygon', type: 'btn', customIcon: 'polygon', tooltip: '绘制区域' },
+      { key: 'divider-2', type: 'divider' },
+      { key: 'save-map', type: 'btn', icon: 'el-icon-check', tooltip: '保存', buttonType: 'primary' }
+    ]
+  }
+  return presets[props.preset] || []
+}
+
+// 获取工具的tooltip
+function getToolTooltip(tool) {
+  return tool.tooltip || tool.key
+}
+
+// 处理需要确认的操作
+async function handleConfirmAction(actionKey) {
+  const confirmMessages = {
+    'init-pose': '确定要进行位姿初始化吗?',
+    'reboot': '确定要重启系统吗?',
+    'emergency-stop': '确定要急停/结束当前操作吗?'
+  }
+
+  const message = confirmMessages[actionKey]
+  if (!message) return
+
+  try {
+    await ElMessageBox.confirm(message, '确认操作', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    const eventMap = {
+      'init-pose': 'confirm-init',
+      'reboot': 'confirm-reboot',
+      'emergency-stop': 'confirm-stop'
+    }
+    emit(eventMap[actionKey])
+  } catch {
+    // 用户取消操作
+  }
+}
+
+// 统一工具点击处理
+function handleToolClick(tool) {
+  // 需要二次确认的操作
+  if (['init-pose', 'reboot', 'emergency-stop'].includes(tool.key)) {
+    handleConfirmAction(tool.key)
+    return
+  }
+
+  // 发送统一点击事件
+  emit('click', { key: tool.key, tool })
+
+  // 兼容特定工具的专用事件
+  if (tool.key === 'zoom-in') {
+    emit('zoom-in')
+  } else if (tool.key === 'zoom-out') {
+    emit('zoom-out')
+  } else if (tool.key === 'center-robot') {
+    emit('center-robot')
+  } else if (tool.key === 'toggle-fullscreen') {
+    emit('toggle-fullscreen')
+  } else if (tool.key === 'add-calibration') {
+    emit('add-calibration-point')
+  } else if (tool.key === 'select-mode') {
+    emit('mode-change', 'select')
+  } else if (tool.key === 'draw-point') {
+    emit('mode-change', 'draw-point')
+  } else if (tool.key === 'draw-line') {
+    emit('mode-change', 'draw-line')
+  } else if (tool.key === 'draw-curve') {
+    emit('mode-change', 'draw-curve')
+  } else if (tool.key === 'draw-polygon') {
+    emit('mode-change', 'draw-polygon')
+  } else if (tool.key === 'save-map') {
+    emit('save')
+  }
+}
+
+// 检查工具是否禁用
+function isToolDisabled(key) {
+  if (props.disabledKeys.includes(key)) {
+    return true
+  }
+  return false
+}
+
+// === 兼容原标定页的方法 ===
+function zoomIn() {
+  emit('zoom-in')
+  emit('click', { key: 'zoom-in' })
+}
+
+function zoomOut() {
+  emit('zoom-out')
+  emit('click', { key: 'zoom-out' })
+}
+
+function centerToRobot() {
+  if (!props.hasRobotPosition) {
+    return
+  }
+  emit('center-to-robot')
+  emit('click', { key: 'center-to-robot' })
+}
+
+function toggleFullscreen() {
+  emit('toggle-fullscreen')
+  emit('click', { key: 'toggle-fullscreen' })
+}
+
+function addCalibrationPoint() {
+  emit('add-calibration-point')
+  emit('click', { key: 'add-calibration-point' })
+}
+
+function resetView() {
+  emit('reset-view')
+  emit('click', { key: 'reset-view' })
+}
+
+function toggleMeasureTool() {
+  emit('toggle-measure-tool')
+  emit('click', { key: 'toggle-measure-tool' })
+}
+</script>
+
+<style>
+@import './_map-shared.css';
+
+/* 工具栏容器 */
+.map-toolbar {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  padding: 8px;
+  pointer-events: auto;
+  width: auto;
+  z-index: 1;
+  animation: toolbarSlideIn 0.3s ease-out;
+}
+
+/* 工具项包装器 */
+.toolbar-item-wrapper {
+  display: contents;
+}
+
+/* 工具项 */
+.toolbar-item {
+  display: flex;
+  flex-direction: inherit;
+  align-items: center;
+  gap: inherit;
+}
+
+/* 工具栏按钮 */
+.toolbar-btn {
+  width: var(--toolbar-btn-size);
+  height: var(--toolbar-btn-size);
+  min-width: var(--toolbar-btn-size);
+  min-height: var(--toolbar-btn-size);
+  padding: 0;
+  margin: 0;
+  border: 1px solid #dcdfe6;
+  background-color: #fff;
+  color: #606266;
+  transition: all 0.2s ease;
+}
+
+.toolbar-btn:hover {
+  background-color: #f5f7fa;
+  border-color: #c0c4cc;
+  color: #409eff;
+}
+
+.toolbar-btn:active {
+  background-color: #e6f7ff;
+  border-color: #409eff;
+  color: #409eff;
+  transform: scale(0.98);
+}
+
+.toolbar-btn.primary-btn {
+  background-color: #409eff;
+  color: #fff;
+  border-color: #409eff;
+}
+
+.toolbar-btn.primary-btn:hover {
+  background-color: #66b1ff;
+  border-color: #66b1ff;
+}
+
+.toolbar-btn.primary-btn:active {
+  background-color: #3a8ee6;
+  border-color: #3a8ee6;
+  transform: scale(0.98);
+}
+
+.toolbar-btn.active {
+  background: #409eff;
+  color: #fff;
+  border-color: #409eff;
+}
+
+.toolbar-btn:disabled {
+  color: #c0c4cc;
+  background-color: #fff;
+  border-color: #ebeef5;
+  cursor: not-allowed;
+}
+
+/* 深度选择器 - Element Plus 图标 */
+.toolbar-btn :deep(.el-icon) {
+  font-size: 16px !important;
+  line-height: 1 !important;
+}
+
+.toolbar-btn :deep(i) {
+  font-size: var(--font-size-base) !important;
+  line-height: 1 !important;
+}
+
+/* 分隔线 */
+.toolbar-divider {
+  width: 100%;
+  height: 1px;
+  margin: 10px 0;
+  background: rgba(0, 0, 0, 0.08);
+  flex-shrink: 0;
+}
+
+/* 自定义图标样式 */
+.custom-icon {
+  width: 16px;
+  height: 16px;
+  display: block;
+  color: currentColor;
+  flex-shrink: 0;
+}
+
+.toolbar-btn:hover .custom-icon,
+.toolbar-btn.active .custom-icon {
+  color: currentColor;
+}
+
+/* 包含自定义图标的按钮 */
+.toolbar-btn.has-custom-icon :deep(.el-button) {
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+  padding: 0 !important;
+}
+
+.toolbar-btn.has-custom-icon :deep(.el-button > span) {
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+  width: 100% !important;
+  height: 100% !important;
+}
+
+/* 暗色主题适配 */
+html.dark .map-toolbar {
+  background: rgba(31, 41, 55, 0.9);
+  border-color: var(--color-border-tertiary);
+}
+
+html.dark .toolbar-btn {
+  background: var(--color-bg-tertiary);
+  border-color: var(--color-border-tertiary);
+  color: var(--color-text-secondary);
+}
+
+html.dark .toolbar-btn:hover {
+  background: var(--color-bg-quaternary);
+  border-color: var(--color-border-secondary);
+  color: var(--color-primary);
+}
+
+html.dark .toolbar-divider {
+  background: var(--color-border-tertiary);
+}
+
+/* 响应式适配 */
+@media (max-width: 768px) {
+  .toolbar-btn {
+    width: var(--toolbar-btn-size-sm) !important;
+    height: var(--toolbar-btn-size-sm) !important;
+    min-width: var(--toolbar-btn-size-sm) !important;
+    min-height: var(--toolbar-btn-size-sm) !important;
+  }
+
+  .toolbar-btn :deep(.el-icon) {
+    font-size: var(--font-size-sm);
+  }
+}
+
+/* 动画效果 */
+@keyframes toolbarSlideIn {
+  from {
+    opacity: 0;
+    transform: translateX(-20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+</style>

+ 3370 - 0
src/views/map/components/shared/RightPanel.vue

@@ -0,0 +1,3370 @@
+<template>
+  <div :class="['right-panel-container', mode === 'nav' ? 'rp--nav' : (mode === 'edit' ? 'rp--edit' : 'rp--calib')]">
+    <!-- 标定页面板(保持原有结构不变) -->
+    <template v-if="panelType !== 'nav'">
+      <!-- 主面板 -->
+      <div
+        class="right-panel"
+        :class="{
+          'is-hidden': !modelValue && (mode === 'edit' || mode === 'nav'),
+          'panel-collapsed': !modelValue && mode === 'calib'
+        }"
+      >
+        <!-- 面板头部 -->
+        <div class="panel-header" v-if="!hideHeader && panelType !== 'edit'">
+          <div class="panel-title">
+            <slot name="header">
+              <h3>{{ title }}</h3>
+              <span v-if="subtitle" class="panel-subtitle">{{ subtitle }}</span>
+            </slot>
+          </div>
+          <el-button
+            @click="togglePanel"
+            type="text"
+            size="small"
+            class="collapse-btn"
+            :icon="modelValue ? 'ArrowRight' : 'ArrowLeft'"
+          />
+        </div>
+
+        <!-- 面板内容 -->
+        <div class="panel-content"> 
+          <!-- 标签页模式 -->
+          <template v-if="tabs.length > 0">
+            <el-tabs v-model="activeTab" @tab-click="handleTabClick" stretch style="padding: 0 16px !important;">
+              <el-tab-pane
+                v-for="tab in processedTabs"
+                :key="tab.key"
+                :label="tab.label"
+                :name="tab.key"
+              >
+                <!-- 实时信息Tab -->
+                <template v-if="tab.key === 'info'">
+                  <div class="nav-tab-content">
+                    <div class="nav-info-grid">
+                      <div class="nav-info-header">
+                        <h3 class="nav-info-title">实时信息</h3>
+                      </div>
+                      <div class="nav-info-content">
+                        <div class="nav-info-item">
+                          <span class="label">当前地图:</span>
+                          <span class="value">{{ realtimeInfo.currentMap }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">坐标:</span>
+                          <span class="value">{{ realtimeInfo.coordinates }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">航向:</span>
+                          <span class="value">{{ realtimeInfo.heading }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">速度:</span>
+                          <span class="value">{{ realtimeInfo.speed }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">指令速度:</span>
+                          <span class="value">{{ realtimeInfo.speedCommand }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">总里程:</span>
+                          <span class="value">{{ realtimeInfo.totalDistance }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">配准误差:</span>
+                          <span class="value">{{ realtimeInfo.registrationError }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">电池电量:</span>
+                          <span class="value">{{ realtimeInfo.batteryLevel }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">当前任务:</span>
+                          <span class="value">{{ realtimeInfo.currentTask }}</span>
+                        </div>
+                      </div>
+                    </div>
+
+                    <!-- 导航和急停状态 -->
+                    <div v-if="mode === 'nav'" class="nav-info-grid" style="margin-top: 16px;">
+                      <div class="nav-info-header">
+                        <h3 class="nav-info-title">系统状态</h3>
+                      </div>
+                      <div class="nav-info-content">
+                        <div class="nav-info-item">
+                          <span class="label">导航状态:</span>
+                          <span class="value" :class="getNavigationStatusClass(navigationStackStatus)">
+                            {{ getNavigationStatusText(navigationStackStatus) }}
+                          </span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">急停状态:</span>
+                          <span class="value" :class="getEmergencyStopClass(emergencyStopEnabled)">
+                            {{ emergencyStopEnabled ? '已启用' : '正常' }}
+                          </span>
+                        </div>
+                      </div>
+                      <div v-if="emergencyStopEnabled" style="padding: 16px; border-top: 1px solid #f0f0f0;">
+                        <el-button
+                          type="success"
+                          native-type="button"
+                          size="small"
+                          :icon="RefreshRight"
+                          @click.prevent="$emit('emergency-stop-release')"
+                          style="width: 100%;"
+                        >
+                          解除急停
+                        </el-button>
+                      </div>
+                    </div>
+
+                    <!-- 当前任务操作 -->
+                    <div v-if="mode === 'nav' && hasActiveNavigation" class="nav-info-grid" style="margin-top: 16px;">
+                      <div class="nav-info-header">
+                        <h3 class="nav-info-title">当前任务操作</h3>
+                      </div>
+                      <div class="nav-info-content">
+                        <div class="nav-info-item">
+                          <span class="label">任务状态:</span>
+                          <span class="value" :class="getCurrentNavigationStatusClass()">
+                            {{ getCurrentNavigationStatusText() }}
+                          </span>
+                        </div>
+                        <div v-if="currentNavigationTask && currentNavigationTask.waypoint" class="nav-info-item">
+                          <span class="label">目标点:</span>
+                          <span class="value">{{ currentNavigationTask.waypoint.name || `点${currentNavigationTask.waypoint.id}` }}</span>
+                        </div>
+                      </div>
+                      <div style="padding: 16px; border-top: 1px solid #f0f0f0;">
+                        <div style="display: flex; gap: 8px;">
+                          <!-- 暂停/恢复按钮 -->
+                          <el-button
+                            :type="navigationStatus === 'paused' ? 'success' : 'warning'"
+                            native-type="button"
+                            size="small"
+                            :icon="navigationStatus === 'paused' ? VideoPlay : VideoPause"
+                            @click.prevent="handleNavigationPauseResume"
+                            style="flex: 1;"
+                            :disabled="navigationStatus === 'planning' || navigationStatus === 'idle'"
+                          >
+                            {{ navigationStatus === 'paused' ? '恢复' : '暂停' }}
+                          </el-button>
+
+                          <!-- 停止按钮 -->
+                          <el-button
+                            type="danger"
+                            native-type="button"
+                            size="small"
+                            :icon="SwitchButton"
+                            @click.prevent="handleNavigationStop"
+                            style="flex: 1;"
+                            :disabled="navigationStatus === 'idle'"
+                          >
+                            停止
+                          </el-button>
+                        </div>
+                      </div>
+                    </div>
+
+                    <!-- 编辑模式下添加"实时位姿"部分 -->
+                    <div v-if="mode === 'edit'" class="nav-info-grid" style="margin-top: 16px;">
+                      <div class="nav-info-header">
+                        <h3 class="nav-info-title">实时位姿</h3>
+                      </div>
+                      <div class="nav-info-content">
+                        <div class="nav-info-item">
+                          <span class="label">X坐标:</span>
+                          <span class="value">{{ realtimeInfo.coordinates.split(',')[0] ? realtimeInfo.coordinates.split(',')[0].replace('(', '').trim() : '0.35' }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">Y坐标:</span>
+                          <span class="value">{{ realtimeInfo.coordinates.split(',')[1] ? realtimeInfo.coordinates.split(',')[1].trim() : '0.22' }}</span>
+                        </div>
+                        <div class="nav-info-item">
+                          <span class="label">Z坐标:</span>
+                          <span class="value">{{ realtimeInfo.coordinates.split(',')[2] ? realtimeInfo.coordinates.split(',')[2].replace(')', '').trim() : '0.22' }}</span>
+                        </div>
+                      </div>
+                      <div style="padding: 16px; border-top: 1px solid #f0f0f0;">
+                        <el-button
+                          size="small"
+                          native-type="button"
+                          :icon="CirclePlus"
+                          @click.prevent="$emit('add-current-point')"
+                          style="width: 100%;"
+                        >
+                          添加当前点
+                        </el-button>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+
+                <!-- 元素管理Tab -->
+                <template v-if="tab.key === 'elements'">
+                  <div class="edit-elements-content">
+                    <!-- 元素类型筛选 -->
+                    <div class="element-filter">
+                      <el-tabs v-model="activeElementType" size="small" @tab-click="handleElementTypeChange">
+                        <el-tab-pane
+                          v-for="type in elementTypes"
+                          :key="type.key"
+                          :label="`${type.label}(${type.count})`"
+                          :name="type.key"
+                        />
+                      </el-tabs>
+                    </div>
+
+                    <!-- 搜索框 -->
+                    <div class="element-search">
+                      <el-input
+                        v-model="elementSearchKeyword"
+                        placeholder="搜索元素..."
+                        :prefix-icon="Search"
+                        size="small"
+                        clearable
+                        @input="handleElementSearch"
+                      />
+                    </div>
+
+                    <!-- 元素列表 -->
+                    <div class="element-list">
+                      <div
+                        v-for="element in filteredElements"
+                        :key="element.id"
+                        class="element-item"
+                        :class="{ 'selected': selectedElement.id === element.id }"
+                        @click="handleElementSelect(element)"
+                      >
+                        <div class="element-info">
+                          <div class="element-id">{{ element.id }}</div>
+                          <div class="element-name">{{ element.name || '未命名' }}</div>
+                        </div>
+                        <div class="element-actions">
+                          <el-button size="small" type="primary" link @click.stop.prevent="$emit('element-edit', element)">编辑</el-button>
+                          <el-button size="small" type="primary" link @click.stop.prevent="$emit('element-locate', element)">定位</el-button>
+                          <el-button
+                            size="small"
+                            type="danger"
+                            link
+                            @click.stop.prevent="$emit('element-remove', element)"
+                          >
+                            删除
+                          </el-button>
+                        </div>
+                      </div>
+
+                      <!-- 空状态 -->
+                      <div v-if="filteredElements.length === 0" class="empty-state">
+                        <i class="el-icon-box"></i>
+                        <p>暂无{{ getCurrentTypeLabel() }}元素</p>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+
+                <!-- 路网操作Tab -->
+                <template v-if="tab.key === 'network'">
+                  <div class="edit-network-content">
+                    <div class="network-operations">
+                      <div class="operation-section">
+                        <h4 class="section-title">路网操作</h4>
+                        <div class="operation-buttons">
+                          <el-button
+                            type="primary"
+                            size="small"
+                            :icon="Download"
+                            @click="handleNetworkOperation('export', $event)"
+                            style="width: 100%;"
+                          >
+                            导出路网
+                          </el-button>
+                          <el-button
+                            size="small"
+                            :icon="Upload"
+                            @click="handleNetworkOperation('import', $event)"
+                            style="width: 100%;"
+                          >
+                            导入路网
+                          </el-button>
+                          <el-button
+                            size="small"
+                            :icon="Plus"
+                            @click="handleNetworkOperation('merge', $event)"
+                            style="width: 100%;"
+                          >
+                            合并导入路网
+                          </el-button>
+                          <el-button
+                            size="small"
+                            :icon="Refresh"
+                            @click="handleNetworkOperation('incremental', $event)"
+                            style="width: 100%;"
+                          >
+                            增量导入路网
+                          </el-button>
+                          <el-button
+                            size="small"
+                            :icon="RefreshLeft"
+                            @click="handleNetworkOperation('overwrite', $event)"
+                            style="width: 100%;"
+                          >
+                            覆盖导入路网
+                          </el-button>
+                        </div>
+                      </div>
+
+                      <div class="operation-section">
+                        <h4 class="section-title">操作说明</h4>
+                        <div class="operation-tips">
+                          <div class="tip-item">
+                            <strong>导出路网:</strong>将当前地图的路网数据导出为文件
+                          </div>
+                          <div class="tip-item">
+                            <strong>导入路网:</strong>导入路网文件到当前地图
+                          </div>
+                          <div class="tip-item">
+                            <strong>合并导入路网:</strong>保留现有路网,添加新的路网数据
+                          </div>
+                          <div class="tip-item">
+                            <strong>增量导入路网:</strong>仅导入不存在的新元素
+                          </div>
+                          <div class="tip-item">
+                            <strong>覆盖导入路网:</strong>清空现有路网,导入新的路网数据
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+
+                <!-- 其他Tab使用插槽 -->
+                <template v-else>
+                  <slot :name="`tab-${tab.key}`">
+                    <!-- 编辑页不需要占位符内容 -->
+                  </slot>
+                </template>
+              </el-tab-pane>
+            </el-tabs>
+
+            <!-- 编辑页面的收起按钮 -->
+            <el-button
+              v-if="mode === 'edit'"
+              class="edit-panel-close-btn"
+              type="text"
+              native-type="button"
+              size="small"
+              :icon="ArrowRight"
+              @click.prevent="$emit('update:modelValue', false)"
+              title="收起面板"
+            />
+          </template>
+
+          <!-- 默认内容模式 -->
+          <template v-else>
+            <slot>
+              <!-- 兼容原标定页内容 -->
+              <div class="default-content">
+                <!-- 实时位姿卡片 -->
+                <div class="info-card" v-if="showPoseCard">
+                  <div class="card-header">
+                    <h4 class="card-title">实时位姿</h4>
+                  </div>
+                  <div class="card-content">
+                    <!-- 激光定位 -->
+                    <div class="pose-section">
+                      <div class="section-title">激光定位</div>
+                      <div class="pose-item">
+                        <span class="pose-label">X坐标</span>
+                        <span class="pose-value">{{ laserPositionData.x }}</span>
+                      </div>
+                      <div class="pose-item">
+                        <span class="pose-label">Y坐标</span>
+                        <span class="pose-value">{{ laserPositionData.y }}</span>
+                      </div>
+                      <div class="pose-item">
+                        <span class="pose-label">航向角</span>
+                        <span class="pose-value">{{ laserPositionData.angle }}</span>
+                      </div>
+                    </div>
+
+                    <!-- GNSS定位 -->
+                    <div class="pose-section">
+                      <div class="section-title">GNSS定位</div>
+                      <div class="pose-item">
+                        <span class="pose-label">状态</span>
+                        <el-tag
+                          :type="getGnssStatusType(gnssPositionData.status)"
+                          size="small"
+                          class="pose-value"
+                        >
+                          {{ gnssPositionData.status }}
+                        </el-tag>
+                      </div>
+                      <div class="pose-item">
+                        <span class="pose-label">经度</span>
+                        <span class="pose-value">{{ gnssPositionData.longitude }}</span>
+                      </div>
+                      <div class="pose-item">
+                        <span class="pose-label">纬度</span>
+                        <span class="pose-value">{{ gnssPositionData.latitude }}</span>
+                      </div>
+                      <div class="pose-item">
+                        <span class="pose-label">方向角</span>
+                        <span class="pose-value">{{ gnssPositionData.angle }}</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+
+                <!-- 当前标定点卡片 -->
+                <div class="info-card" v-if="showCalibrationCard">
+                  <div class="card-header">
+                    <h4 class="card-title">当前标定点</h4>
+                  </div>
+                  <div class="card-content">
+                    <div class="calibration-table" v-if="calibrationList.length > 0">
+                      <div class="table-header">
+                        <div class="col-id">序号</div>
+                        <div class="col-coord">坐标(X,Y)</div>
+                        <div class="col-action">操作</div>
+                      </div>
+                      <div class="table-body">
+                        <div
+                          v-for="item in calibrationList"
+                          :key="item.id"
+                          class="table-row"
+                        >
+                          <div class="col-id">{{ item.id }}</div>
+                          <div class="col-coord">{{ item.coordinate }}</div>
+                          <div class="col-action">
+                            <el-popconfirm
+                              title="确定要删除这个标定点吗?"
+                              @confirm="removeCalibration(item.id)"
+                              placement="left"
+                            >
+                              <template #reference>
+                                <el-button
+                                  type="primary"
+                                  link
+                                  :icon="Delete"
+                                  class="delete-btn"
+                                />
+                              </template>
+                            </el-popconfirm>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+
+                    <!-- 空状态 -->
+                    <div v-else class="empty-state">
+                      <i class="el-icon-position"></i>
+                      <p>暂无标定点</p>
+                    </div>
+
+                    <!-- 操作按钮组 -->
+                    <div class="action-buttons">
+                      <el-button
+                        type="primary"
+                        size="small"
+                        :icon="Plus"
+                        @click="addCalibration"
+                        :disabled="!canAddCalibration"
+                      >
+                        添加标定点
+                      </el-button>
+                      <el-button
+                        size="small"
+                        :icon="Check"
+                        @click="executeCalibration"
+                        :disabled="calibrationList.length === 0"
+                      >
+                        一键标定
+                      </el-button>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </slot>
+          </template>
+        </div>
+
+        <!-- 拖拽调宽手柄 -->
+        <div
+          class="resize-handle"
+          @mousedown="startResize"
+          v-if="resizable && !isDrawerMode && modelValue"
+        ></div>
+      </div>
+
+      <!-- 展开拉手 (收起时显示) -->
+      <div
+        v-if="overlay && !modelValue"
+        class="expand-handle edit-expand-handle"
+        @click="$emit('update:modelValue', true)"
+        title="展开面板"
+        style="position: fixed !important; right: 0 !important; top: 50% !important; transform: translateY(-50%) !important; z-index: 9999 !important; width: 36px !important; height: 72px !important; background: var(--color-bg-card, #ffffff) !important; border: 1px solid var(--color-border-primary, #dcdfe6) !important; border-radius: 8px 0 0 8px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; transition: all 0.3s ease !important;"
+      >
+        <el-icon :size="16" style="color: var(--color-text-primary, #303133); font-weight: bold;">
+          <ArrowLeft />
+        </el-icon>
+      </div>
+    </template>
+
+    <!-- 导航页面板(全新独立结构) -->
+    <template v-else>
+      <aside
+        class="right-panel nav-right-panel"
+        :class="{ 'is-hidden': !visible }"
+      >
+        <!-- 导航页Tabs -->
+        <div class="nav-panel-header">
+          <el-tabs v-model="navActiveTab" class="nav-tabs">
+            <!-- Tab1: 实时信息 -->
+            <el-tab-pane label="实时信息" name="realtime">
+              <div class="nav-tab-content">
+                <div class="nav-info-grid">
+                  <div class="nav-info-header">
+                    <h3 class="nav-info-title">实时信息</h3>
+                  </div>
+                  <div class="nav-info-content">
+                    <div class="nav-info-item">
+                      <span class="label">当前地图:</span>
+                      <span class="value">{{ realtimeInfo.currentMap }}</span>
+                    </div>
+                    <div class="nav-info-item">
+                      <span class="label">坐标:</span>
+                      <span class="value">{{ realtimeInfo.coordinates }}</span>
+                    </div>
+                    <div class="nav-info-item">
+                      <span class="label">航向:</span>
+                      <span class="value">{{ realtimeInfo.heading }}</span>
+                    </div>
+                    <div class="nav-info-item">
+                      <span class="label">速度:</span>
+                      <span class="value">{{ realtimeInfo.speed }}</span>
+                    </div>
+                    <div class="nav-info-item">
+                      <span class="label">指令速度:</span>
+                      <span class="value">{{ realtimeInfo.speedCommand }}</span>
+                    </div>
+                    <div class="nav-info-item">
+                      <span class="label">总里程:</span>
+                      <span class="value">{{ realtimeInfo.totalDistance }}</span>
+                    </div>
+                    <div class="nav-info-item">
+                      <span class="label">配准误差:</span>
+                      <span class="value">{{ realtimeInfo.registrationError }}</span>
+                    </div>
+                    <!-- 电池信息区域 -->
+                    <div class="nav-info-item battery-item">
+                      <span class="label">电池电量:</span>
+                      <div class="battery-display">
+                        <span class="value battery-percentage" :class="getBatteryClass()">
+                          {{ realtimeInfo.batteryLevel || '0%' }}
+                        </span>
+                        <span v-if="realtimeInfo.batteryDetails && realtimeInfo.batteryDetails.charging" class="charging-icon">⚡</span>
+                      </div>
+                    </div>
+                    <!-- 电池详细信息(可折叠展开) -->
+                    <template v-if="realtimeInfo.batteryDetails && realtimeInfo.batteryDetails.capacity">
+                      <div class="nav-info-item battery-detail">
+                        <span class="label">电池温度:</span>
+                        <span class="value">{{ realtimeInfo.batteryDetails.temperature }}°C</span>
+                      </div>
+                      <div class="nav-info-item battery-detail">
+                        <span class="label">电池电压:</span>
+                        <span class="value">{{ realtimeInfo.batteryDetails.voltage }}V</span>
+                      </div>
+                      <div class="nav-info-item battery-detail">
+                        <span class="label">电池电流:</span>
+                        <span class="value">{{ realtimeInfo.batteryDetails.current }}A</span>
+                      </div>
+                      <div class="nav-info-item battery-detail">
+                        <span class="label">充电状态:</span>
+                        <span class="value" :class="realtimeInfo.batteryDetails.charging ? 'charging-text' : ''">
+                          {{ realtimeInfo.batteryDetails.charging ? '充电中' : '未充电' }}
+                        </span>
+                      </div>
+                    </template>
+                    <div class="nav-info-item">
+                      <span class="label">当前任务:</span>
+                      <span class="value">{{ realtimeInfo.currentTask }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+
+            <!-- Tab2: 功能设置 -->
+            <el-tab-pane label="功能设置" name="settings">
+              <div class="nav-tab-content">
+                <div class="settings-list">
+                  <div class="setting-item">
+                    <div class="setting-info">
+                      <span class="setting-label">点云</span>
+                      <span class="setting-desc">显示/隐藏点云数据</span>
+                    </div>
+                    <el-switch
+                      v-model="settingParams.pointCloud"
+                      @change="(val) => onSettingChange('pointCloud', val)"
+                    />
+                  </div>
+
+                  <div class="setting-item">
+                    <div class="setting-info">
+                      <span class="setting-label">底图</span>
+                      <span class="setting-desc">显示/隐藏地图底图</span>
+                    </div>
+                    <el-switch
+                      v-model="settingParams.baseMap"
+                      @change="(val) => onSettingChange('baseMap', val)"
+                    />
+                  </div>
+
+                  <div class="setting-item">
+                    <div class="setting-info">
+                      <span class="setting-label">点ID</span>
+                      <span class="setting-desc">显示/隐藏点位ID标识</span>
+                    </div>
+                    <el-switch
+                      v-model="settingParams.pointId"
+                      @change="(val) => onSettingChange('pointId', val)"
+                    />
+                  </div>
+
+                  <div class="setting-item">
+                    <div class="setting-info">
+                      <span class="setting-label">位置跟随</span>
+                      <span class="setting-desc">地图自动跟随机器人位置</span>
+                    </div>
+                    <el-switch
+                      v-model="settingParams.follow"
+                      @change="(val) => onSettingChange('follow', val)"
+                    />
+                  </div>
+
+                  <div class="setting-item">
+                    <div class="setting-info">
+                      <span class="setting-label">网络邻居</span>
+                      <span class="setting-desc">显示网络邻居设备</span>
+                    </div>
+                    <el-switch
+                      v-model="settingParams.network"
+                      @change="(val) => onSettingChange('network', val)"
+                    />
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+
+            <!-- Tab3: 目标点 -->
+            <el-tab-pane label="目标点" name="waypoint">
+              <div class="nav-tab-content">
+                <!-- 操作工具栏 -->
+                <div class="waypoint-toolbar">
+                  <!-- 2行3列网格布局 -->
+                  <div class="toolbar-grid">
+                    <!-- 第一行:上移、下移、批量删除 -->
+                    <el-button type="primary" native-type="button" size="small" :disabled="waypointSingle" @click.prevent="$emit('wp-move-up')" class="grid-btn">
+                      <el-icon><ArrowUp /></el-icon>
+                      <span>上移</span>
+                    </el-button>
+                    <el-button type="primary" native-type="button" size="small" :disabled="waypointSingle" @click.prevent="$emit('wp-move-down')" class="grid-btn">
+                      <el-icon><ArrowDown /></el-icon>
+                      <span>下移</span>
+                    </el-button>
+                    <el-popconfirm title="确定批量删除选中的目标点?" @confirm="$emit('wp-batch-remove')">
+                      <template #reference>
+                        <el-button type="danger" native-type="button" size="small" :disabled="waypointMultiple" class="grid-btn">
+                          <el-icon><Delete /></el-icon>
+                          <span>批量删除</span>
+                        </el-button>
+                      </template>
+                    </el-popconfirm>
+
+                    <!-- 第二行:立即前往、生成任务、地图选点 -->
+                    <el-button type="warning" native-type="button" size="small" :disabled="waypointSingle" @click.prevent="$emit('wp-goto')" class="grid-btn">
+                      <el-icon><Position /></el-icon>
+                      <span>立即前往</span>
+                    </el-button>
+                    <el-button type="success" native-type="button" size="small" :disabled="waypointMultiple" @click.prevent="$emit('wp-create-task')" class="grid-btn">
+                      <el-icon><Plus /></el-icon>
+                      <span>生成任务</span>
+                    </el-button>
+                    <el-button
+                      :type="mapSelectMode ? 'danger' : 'info'"
+                      native-type="button"
+                      size="small"
+                      @click.prevent="toggleMapSelectMode"
+                      class="grid-btn"
+                    >
+                      <el-icon><component :is="mapSelectMode ? 'Close' : 'Location'" /></el-icon>
+                      <span>{{ mapSelectMode ? '关闭选点' : '地图选点' }}</span>
+                    </el-button>
+                  </div>
+                </div>
+
+                <!-- 目标点列表 -->
+                <div class="nav-list-content nav-list-scrollable">
+                  <div v-if="waypointList.length > 0" class="nav-waypoint-list">
+                    <div
+                      v-for="waypoint in waypointList"
+                      :key="waypoint.id"
+                      class="nav-waypoint-item"
+                      :class="{ 'selected': selectedWaypoints.some(w => w.id === waypoint.id) }"
+                      @click="onWaypointItemClick(waypoint)"
+                    >
+                      <div class="waypoint-checkbox" @click.stop="handleCheckboxClick(waypoint)">
+                        <div
+                          class="custom-checkbox"
+                          :class="{ 'checked': selectedWaypoints.some(w => w.id === waypoint.id) }"
+                          @click.stop="handleCheckboxClick(waypoint)"
+                        >
+                          <el-icon v-if="selectedWaypoints.some(w => w.id === waypoint.id)" :size="10"><Check /></el-icon>
+                        </div>
+                      </div>
+                      <div class="waypoint-info">
+                        <div class="waypoint-main">
+                          <span class="waypoint-id">编号: {{ waypoint.id }}</span>
+                          <span class="waypoint-coords">坐标: ({{ waypoint.x }}, {{ waypoint.y }})</span>
+                        </div>
+                      </div>
+                      <div class="waypoint-actions">
+                        <el-button size="small" type="primary" link @click.stop.prevent="$emit('wp-edit', waypoint)" class="action-btn">编辑</el-button>
+                        <el-popconfirm title="删除当前目标点?" @confirm="$emit('wp-remove', waypoint)">
+                          <template #reference>
+                            <el-button size="small" type="danger" link @click.stop.prevent class="action-btn">删除</el-button>
+                          </template>
+                        </el-popconfirm>
+                      </div>
+                    </div>
+                  </div>
+
+                  <!-- 空状态 -->
+                  <div v-else class="nav-empty-state">
+                    <el-icon :size="48"><Location /></el-icon>
+                    <p>暂无目标点</p>
+                    <p class="empty-hint">点击上方工具栏中的"地图选点"按钮在地图上添加目标点</p>
+                  </div>
+                </div>
+
+                <!-- 底部快捷操作 -->
+                <div v-if="selectedWaypoints.length > 0" class="waypoint-quick-actions">
+                  <div class="quick-actions-info">
+                    <span>已选择 {{ selectedWaypoints.length }} 个目标点</span>
+                  </div>
+                  <div class="quick-actions-buttons">
+                    <el-button size="small" @click="clearSelection">取消选择</el-button>
+                  </div>
+                </div>
+
+                <!-- 当前任务操作 -->
+                <div v-if="hasActiveNavigation" class="nav-info-grid" style="margin-top: 16px;">
+                  <div class="nav-info-header">
+                    <h3 class="nav-info-title">当前任务操作</h3>
+                  </div>
+                  <div class="nav-info-content">
+                    <div class="nav-info-item">
+                      <span class="label">任务状态:</span>
+                      <span class="value" :class="getCurrentNavigationStatusClass()">
+                        {{ getCurrentNavigationStatusText() }}
+                      </span>
+                    </div>
+                    <div v-if="currentNavigationTask && currentNavigationTask.waypoint" class="nav-info-item">
+                      <span class="label">目标点:</span>
+                      <span class="value">{{ currentNavigationTask.waypoint.name || `点${currentNavigationTask.waypoint.id}` }}</span>
+                    </div>
+                  </div>
+                  <div style="padding: 16px; border-top: 1px solid #f0f0f0;">
+                    <div style="display: flex; gap: 8px;">
+                      <!-- 暂停/恢复按钮 -->
+                      <el-button
+                        :type="navigationStatus === 'paused' ? 'success' : 'warning'"
+                        native-type="button"
+                        size="small"
+                        :icon="navigationStatus === 'paused' ? VideoPlay : VideoPause"
+                        @click.prevent="handleNavigationPauseResume"
+                        style="flex: 1;"
+                        :disabled="navigationStatus === 'planning' || navigationStatus === 'idle'"
+                      >
+                        {{ navigationStatus === 'paused' ? '恢复' : '暂停' }}
+                      </el-button>
+
+                      <!-- 停止按钮 -->
+                      <el-button
+                        type="danger"
+                        native-type="button"
+                        size="small"
+                        :icon="SwitchButton"
+                        @click.prevent="handleNavigationStop"
+                        style="flex: 1;"
+                        :disabled="navigationStatus === 'idle'"
+                      >
+                        停止
+                      </el-button>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+
+            <!-- Tab4: 任务 -->
+            <el-tab-pane label="任务" name="task">
+              <div class="nav-tab-content">
+
+                <div class="nav-list-content">
+                  <div v-if="taskList.length > 0" class="nav-task-list">
+                    <div
+                      v-for="task in taskList"
+                      :key="task.taskId"
+                      class="nav-task-item"
+                    >
+                      <div class="task-info">
+                        <span class="task-name">{{ task.taskName }}</span>
+                        <span class="task-status" :class="'status-' + task.status">{{ getTaskStatusText(task.status) }}</span>
+                      </div>
+                      <div class="task-actions">
+                        <el-button size="small" type="primary" link @click.prevent="$emit('task-view', task)">查看</el-button>
+                        <el-button
+                          v-if="task.status !== 'running' && task.status !== 'paused'"
+                          size="small"
+                          type="primary"
+                          link
+                          @click.prevent="$emit('task-edit', task)"
+                        >
+                          编辑
+                        </el-button>
+                        <el-button
+                          v-if="task.status !== 'running' && task.status !== 'paused'"
+                          size="small"
+                          type="success"
+                          link
+                          @click.prevent="$emit('task-start', task)"
+                        >
+                          开始
+                        </el-button>
+                        <el-button
+                          v-if="task.status === 'running'"
+                          size="small"
+                          type="warning"
+                          link
+                          @click.prevent="$emit('task-pause', task)"
+                        >
+                          暂停
+                        </el-button>
+                        <el-button
+                          v-if="task.status === 'paused'"
+                          size="small"
+                          type="success"
+                          link
+                          @click.prevent="$emit('task-resume', task)"
+                        >
+                          继续
+                        </el-button>
+                        <el-button
+                          v-if="task.status === 'running' || task.status === 'paused'"
+                          size="small"
+                          type="danger"
+                          link
+                          @click.prevent="$emit('task-stop', task)"
+                        >
+                          停止
+                        </el-button>
+                        <el-button
+                          v-if="task.status !== 'running' && task.status !== 'paused'"
+                          size="small"
+                          type="danger"
+                          link
+                          @click.prevent="$emit('task-remove', task)"
+                        >
+                          删除
+                        </el-button>
+                      </div>
+                    </div>
+                  </div>
+                  <div v-else class="nav-empty-state">
+                    <el-icon :size="48"><Tickets /></el-icon>
+                    <p>暂无任务</p>
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+
+          </el-tabs>
+
+          <!-- 导航页面的收起按钮 -->
+          <el-button
+            v-if="panelType === 'nav'"
+            class="nav-panel-close-btn"
+            type="text"
+            native-type="button"
+            size="small"
+            :icon="ArrowRight"
+            @click.prevent="$emit('update:visible', false)"
+            title="收起面板"
+          />
+        </div>
+
+      </aside>
+
+      <!-- 导航页展开拉手 (收起时显示) - 复用标定页面逻辑 -->
+      <div
+        v-if="overlay && !visible"
+        class="expand-handle nav-expand-handle"
+        @click="$emit('update:visible', true)"
+        title="展开面板"
+        style="position: fixed !important; right: 0 !important; top: 50% !important; transform: translateY(-50%) !important; z-index: 9999 !important; width: 36px !important; height: 72px !important; background: var(--color-bg-card, #ffffff) !important; border: 1px solid var(--color-border-primary, #dcdfe6) !important; border-radius: 8px 0 0 8px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; transition: all 0.3s ease !important;"
+      >
+        <el-icon :size="16" style="color: var(--color-text-primary, #303133); font-weight: bold;">
+          <ArrowLeft />
+        </el-icon>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  ArrowRight,
+  ArrowLeft,
+  ArrowUp,
+  ArrowDown,
+  RefreshRight,
+  RefreshLeft,
+  Refresh,
+  VideoPlay,
+  VideoPause,
+  SwitchButton,
+  CirclePlus,
+  Search,
+  Download,
+  Upload,
+  Plus,
+  Delete,
+  Check,
+  Location,
+  Position,
+  Close,
+  Tickets
+} from '@element-plus/icons-vue'
+
+// Props 定义
+const props = defineProps({
+  // 面板模式 - 区分导航/标定/编辑
+  mode: {
+    type: String,
+    default: 'calib', // 'calib' 或 'nav' 或 'edit'
+    validator: (value) => ['calib', 'nav', 'edit'].includes(value)
+  },
+  // 面板类型 - 新增
+  panelType: {
+    type: String,
+    default: 'calibration'
+  },
+  // v-model 支持
+  modelValue: {
+    type: Boolean,
+    default: true
+  },
+  // 面板是否可见 - 新增(导航页面板控制)
+  visible: {
+    type: Boolean,
+    default: true
+  },
+  // 面板宽度
+  width: {
+    type: Number,
+    default: 360
+  },
+  // 标签页配置 - 支持字符串数组或对象数组
+  tabs: {
+    type: Array,
+    default: () => []
+  },
+  // 初始标签页
+  initialTab: {
+    type: String,
+    default: ''
+  },
+  // 收起后是否保留拉手
+  overlay: {
+    type: Boolean,
+    default: true
+  },
+  // 是否可调整大小
+  resizable: {
+    type: Boolean,
+    default: true
+  },
+  // 面板标题
+  title: {
+    type: String,
+    default: '面板'
+  },
+  // 面板副标题
+  subtitle: {
+    type: String,
+    default: ''
+  },
+  // 是否隐藏头部
+  hideHeader: {
+    type: Boolean,
+    default: false
+  },
+  // 展开提示文本
+  expandTooltip: {
+    type: String,
+    default: '展开面板'
+  },
+
+  // === 兼容原标定页的props ===
+  laserPositionData: {
+    type: Object,
+    default: () => ({ x: 0, y: 0, angle: 0 })
+  },
+  gnssPositionData: {
+    type: Object,
+    default: () => ({ status: '0/0', longitude: 0, latitude: 0, angle: 0 })
+  },
+  calibrationList: {
+    type: Array,
+    default: () => []
+  },
+  currentMap: {
+    type: Object,
+    default: () => ({ name: 'sh02' })
+  },
+  isDrawerMode: {
+    type: Boolean,
+    default: false
+  },
+  showPoseCard: {
+    type: Boolean,
+    default: true
+  },
+  showCalibrationCard: {
+    type: Boolean,
+    default: true
+  },
+
+  // === 实时信息相关props ===
+  realtimeInfo: {
+    type: Object,
+    default: () => ({
+      currentMap: '',
+      currentTask: '暂无',
+      speed: '',
+      speedCommand: '',
+      coordinates: '(0.000, 0.000, 0.000)',
+      heading: '0°',
+      totalDistance: '0m',
+      registrationError: '0',
+      batteryLevel: '0%'
+    })
+  },
+
+  // === 导航页专用props ===
+  waypointList: {
+    type: Array,
+    default: () => []
+  },
+  taskList: {
+    type: Array,
+    default: () => []
+  },
+  settingParams: {
+    type: Object,
+    default: () => ({
+      pointCloud: false,
+      baseMap: true,
+      pointId: false,
+      follow: false,
+      network: false
+    })
+  },
+  // 急停状态
+  emergencyStopEnabled: {
+    type: Boolean,
+    default: false
+  },
+  // 导航堆栈状态
+  navigationStackStatus: {
+    type: String,
+    default: 'unknown'
+  },
+  // 当前导航任务
+  currentNavigationTask: {
+    type: Object,
+    default: () => null
+  },
+  // 导航状态
+  navigationStatus: {
+    type: String,
+    default: 'idle' // idle, planning, navigating, paused, arrived, failed
+  },
+  // 是否正在导航
+  isNavigating: {
+    type: Boolean,
+    default: false
+  },
+
+  // === 编辑页专用props ===
+  elementList: {
+    type: Array,
+    default: () => []
+  },
+  selectedElement: {
+    type: Object,
+    default: () => ({})
+  },
+  elementTypes: {
+    type: Array,
+    default: () => [
+      { key: 'point', label: '点', count: 0 },
+      { key: 'line', label: '线', count: 0 },
+      { key: 'curve', label: '弧', count: 0 },
+      { key: 'polygon', label: '面', count: 0 }
+    ]
+  }
+})
+
+// Emits 定义
+const emit = defineEmits([
+  'input',
+  'update:modelValue',
+  'update:visible',
+  'tab-change',
+  'panel-resize',
+  'add-calibration',
+  'remove-calibration',
+  'execute-calibration',
+  'setting-change',
+  'wp-selection-change',
+  'map-select-mode-change',
+  'wp-move-up',
+  'wp-move-down',
+  'wp-batch-remove',
+  'wp-goto',
+  'wp-create-task',
+  'wp-edit',
+  'wp-remove',
+  'wp-add',
+  'element-edit',
+  'element-locate',
+  'element-remove',
+  'element-select',
+  'element-type-change',
+  'element-search',
+  'network-export',
+  'network-import',
+  'page-change',
+  'emergency-stop-release',
+  'add-current-point',
+  'task-view',
+  'task-edit',
+  'task-start',
+  'task-pause',
+  'task-resume',
+  'task-stop',
+  'task-remove',
+  'navigation-pause',
+  'navigation-resume',
+  'navigation-stop'
+])
+
+// 响应式数据
+const activeTab = ref('')
+const navActiveTab = ref('realtime') // 导航页默认激活的tab
+const isResizing = ref(false)
+const startX = ref(0)
+const startWidth = ref(0)
+// 目标点相关状态
+const selectedWaypoints = ref([]) // 选中的目标点
+const mapSelectMode = ref(false) // 地图选点模式
+// 编辑页相关状态
+const activeElementType = ref('point') // 当前选中的元素类型
+const elementSearchKeyword = ref('') // 元素搜索关键词
+
+// 计算属性
+const canAddCalibration = computed(() => {
+  return props.laserPositionData.x !== 0 || props.laserPositionData.y !== 0
+})
+
+// 目标点操作按钮状态
+const waypointSingle = computed(() => {
+  return selectedWaypoints.value.length !== 1
+})
+const waypointMultiple = computed(() => {
+  return selectedWaypoints.value.length === 0
+})
+
+// 是否有活动的导航任务
+const hasActiveNavigation = computed(() => {
+  return props.currentNavigationTask !== null &&
+    props.navigationStatus !== 'idle' &&
+    props.navigationStatus !== 'arrived' &&
+    props.navigationStatus !== 'failed'
+})
+
+// 处理Tab配置,支持字符串数组和对象数组
+const processedTabs = computed(() => {
+  const tabMap = {
+    'info': { key: 'info', label: '实时信息' },
+    'settings': { key: 'settings', label: '功能设置' },
+    'task': { key: 'task', label: '任务配置' },
+    'points': { key: 'points', label: '目标点' },
+    'system': { key: 'system', label: '系统状态' },
+    'elements': { key: 'elements', label: '元素管理' },
+    'network': { key: 'network', label: '路网操作' }
+  }
+
+  return props.tabs.map(tab => {
+    if (typeof tab === 'string') {
+      return tabMap[tab] || { key: tab, label: tab }
+    }
+    return tab
+  })
+})
+
+// 编辑模式相关计算属性
+const filteredElements = computed(() => {
+  if (!props.elementList || props.elementList.length === 0) {
+    return []
+  }
+
+  let filtered = props.elementList
+
+  // 按类型筛选
+  if (activeElementType.value !== 'all') {
+    filtered = filtered.filter(element => {
+      if (activeElementType.value === 'point') return element.id.startsWith('p')
+      if (activeElementType.value === 'line') return element.id.startsWith('l')
+      if (activeElementType.value === 'curve') return element.id.startsWith('b')
+      if (activeElementType.value === 'polygon') return element.id.startsWith('s')
+      return true
+    })
+  }
+
+  // 按关键词搜索
+  if (elementSearchKeyword.value) {
+    const keyword = elementSearchKeyword.value.toLowerCase()
+    filtered = filtered.filter(element =>
+      element.id.toLowerCase().includes(keyword) ||
+      (element.name && element.name.toLowerCase().includes(keyword))
+    )
+  }
+
+  return filtered
+})
+
+// 监听目标点列表变化,自动清理无效选择
+watch(() => props.waypointList, (newList) => {
+  if (selectedWaypoints.value.length > 0) {
+    // 过滤出仍然存在的选中项
+    const validSelections = selectedWaypoints.value.filter(selected =>
+      newList.some(waypoint => waypoint.id === selected.id)
+    )
+
+    // 如果选择发生了变化,更新选择状态
+    if (validSelections.length !== selectedWaypoints.value.length) {
+      selectedWaypoints.value = validSelections
+      emit('wp-selection-change', selectedWaypoints.value)
+    }
+  }
+}, { deep: true })
+
+// 初始化标签页
+onMounted(() => {
+  if (props.tabs.length > 0) {
+    const firstTab = processedTabs.value[0]
+    activeTab.value = props.initialTab || (firstTab ? firstTab.key : '')
+  }
+
+  // 添加全局鼠标事件监听
+  document.addEventListener('mousemove', handleResize)
+  document.addEventListener('mouseup', stopResize)
+})
+
+// 清理事件监听
+onBeforeUnmount(() => {
+  document.removeEventListener('mousemove', handleResize)
+  document.removeEventListener('mouseup', stopResize)
+})
+
+// 方法
+// 切换面板展开/收起
+const togglePanel = () => {
+  emit('update:modelValue', !props.modelValue)
+}
+
+// 展开面板
+const expandPanel = () => {
+  emit('update:modelValue', true)
+}
+
+// 标签页切换
+const handleTabClick = (tab) => {
+  activeTab.value = tab.props.name
+  emit('tab-change', tab.props.name)
+}
+
+// 开始拖拽调整大小
+const startResize = (event) => {
+  if (props.isDrawerMode || !props.resizable) return
+
+  isResizing.value = true
+  startX.value = event.clientX
+  startWidth.value = props.width
+  document.body.style.cursor = 'col-resize'
+  document.body.style.userSelect = 'none'
+}
+
+// 处理拖拽调整大小
+const handleResize = (event) => {
+  if (!isResizing.value || props.isDrawerMode) return
+
+  const diff = startX.value - event.clientX
+  const newWidth = Math.max(320, Math.min(420, startWidth.value + diff))
+
+  if (newWidth !== props.width) {
+    emit('panel-resize', newWidth)
+  }
+}
+
+// 停止拖拽调整大小
+const stopResize = () => {
+  if (isResizing.value) {
+    isResizing.value = false
+    document.body.style.cursor = ''
+    document.body.style.userSelect = ''
+  }
+}
+
+// === 兼容原标定页的方法 ===
+const getGnssStatusType = (status) => {
+  if (status === '0/0') return 'info'
+  if (status.includes('锁定')) return 'success'
+  if (status.includes('异常')) return 'warning'
+  return 'info'
+}
+
+const addCalibration = () => {
+  emit('add-calibration')
+}
+
+const removeCalibration = (id) => {
+  emit('remove-calibration', id)
+}
+
+const executeCalibration = () => {
+  emit('execute-calibration')
+}
+
+// === 导航页专用方法 ===
+const getTaskStatusText = (status) => {
+  const statusMap = {
+    'idle': '空闲',
+    'running': '运行中',
+    'paused': '暂停',
+    'completed': '已完成',
+    'failed': '失败'
+  }
+  return statusMap[status] || '未知'
+}
+
+// 获取电池状态样式类
+const getBatteryClass = () => {
+  if (!props.realtimeInfo.batteryDetails) return ''
+  const capacity = props.realtimeInfo.batteryDetails.capacity || 0
+  if (props.realtimeInfo.batteryDetails.charging) return 'battery-charging'
+  if (capacity <= 0.2) return 'battery-low'
+  if (capacity <= 0.5) return 'battery-medium'
+  return 'battery-normal'
+}
+
+// 设置项变更处理
+const onSettingChange = (settingKey, value) => {
+  console.log(`设置项 ${settingKey} 变更为:`, value)
+  emit('setting-change', { key: settingKey, value: value })
+}
+
+// === 目标点相关方法 ===
+
+// 目标点表格选择变更
+const onWaypointSelectionChange = (selection) => {
+  selectedWaypoints.value = selection
+  emit('wp-selection-change', selection)
+}
+
+// 切换地图选点模式
+const toggleMapSelectMode = () => {
+  mapSelectMode.value = !mapSelectMode.value
+  emit('map-select-mode-change', mapSelectMode.value)
+}
+
+// 目标点项点击
+const onWaypointItemClick = (waypoint) => {
+  // 单击选择/取消选择
+  const isSelected = selectedWaypoints.value.some(w => w.id === waypoint.id)
+  if (isSelected) {
+    selectedWaypoints.value = selectedWaypoints.value.filter(w => w.id !== waypoint.id)
+  } else {
+    selectedWaypoints.value.push(waypoint)
+  }
+  emit('wp-selection-change', selectedWaypoints.value)
+}
+
+// 自定义复选框点击处理
+const handleCheckboxClick = (waypoint) => {
+  console.log('Custom checkbox clicked for waypoint:', waypoint.id)
+  const isSelected = selectedWaypoints.value.some(w => w.id === waypoint.id)
+  console.log('Current selection state:', isSelected, 'Toggling to:', !isSelected)
+
+  // 直接处理选择状态变更
+  if (isSelected) {
+    // 取消选择
+    selectedWaypoints.value = selectedWaypoints.value.filter(w => w.id !== waypoint.id)
+    console.log('Removed waypoint from selection')
+  } else {
+    // 添加选择
+    selectedWaypoints.value.push(waypoint)
+    console.log('Added waypoint to selection')
+  }
+
+  // 通知父组件
+  emit('wp-selection-change', selectedWaypoints.value)
+  console.log('Final selection:', selectedWaypoints.value.map(w => w.id))
+}
+
+// 清除选择
+const clearSelection = () => {
+  selectedWaypoints.value = []
+  emit('wp-selection-change', selectedWaypoints.value)
+}
+
+// 获取路径类型文本
+const getWaypointTypeText = (type) => {
+  const typeMap = {
+    0: '自由路径',
+    1: '路网路径'
+  }
+  return typeMap[type] || '未知'
+}
+
+// === 编辑页专用方法 ===
+
+// 处理元素类型切换
+const handleElementTypeChange = (tab) => {
+  activeElementType.value = tab.props.name
+  emit('element-type-change', tab.props.name)
+}
+
+// 处理元素搜索(防抖)
+let searchTimer = null
+const handleElementSearch = () => {
+  if (searchTimer) {
+    clearTimeout(searchTimer)
+  }
+  searchTimer = setTimeout(() => {
+    emit('element-search', elementSearchKeyword.value)
+  }, 300)
+}
+
+// 处理元素选择
+const handleElementSelect = (element) => {
+  emit('element-select', element)
+}
+
+// 处理路网操作
+const handleNetworkOperation = (operation, event) => {
+  const operationMap = {
+    'export': {
+      title: '导出路网',
+      message: '确定要导出当前地图的路网数据吗?',
+      confirmText: '确定导出',
+      successMsg: '路网导出成功!',
+      errorMsg: '路网导出失败!'
+    },
+    'import': {
+      title: '导入路网',
+      message: '确定要导入路网文件吗?这将会添加新的路网数据到当前地图。',
+      confirmText: '确定导入',
+      successMsg: '路网导入成功!',
+      errorMsg: '路网导入失败!'
+    },
+    'merge': {
+      title: '合并导入路网',
+      message: '确定要合并导入路网吗?这将保留现有路网,并添加新的路网数据。',
+      confirmText: '确定合并',
+      successMsg: '路网合并导入成功!',
+      errorMsg: '路网合并导入失败!'
+    },
+    'incremental': {
+      title: '增量导入路网',
+      message: '确定要增量导入路网吗?这将只导入不存在的新元素。',
+      confirmText: '确定导入',
+      successMsg: '路网增量导入成功!',
+      errorMsg: '路网增量导入失败!'
+    },
+    'overwrite': {
+      title: '覆盖导入路网',
+      message: '确定要覆盖导入路网吗?这将清空现有路网,导入新的路网数据。此操作不可恢复!',
+      confirmText: '确定覆盖',
+      successMsg: '路网覆盖导入成功!',
+      errorMsg: '路网覆盖导入失败!'
+    }
+  }
+
+  const config = operationMap[operation]
+  if (!config) return
+
+  // 获取触发事件的按钮元素
+  const button = event ? event.currentTarget : null
+
+  ElMessageBox.confirm(config.message, config.title, {
+    confirmButtonText: config.confirmText,
+    cancelButtonText: '取消',
+    type: operation === 'overwrite' ? 'warning' : 'info'
+  }).then(() => {
+    // 发射事件给父组件处理具体逻辑
+    if (operation === 'export') {
+      emit('network-export')
+    } else {
+      emit('network-import', operation === 'import' ? 'import' : operation === 'overwrite' ? 'replace' : operation)
+    }
+
+    // 模拟异步操作,实际应该在父组件中处理
+    setTimeout(() => {
+      ElMessage.success(config.successMsg)
+      // 移除按钮焦点
+      if (button) {
+        button.blur()
+      }
+    }, 500)
+  }).catch(() => {
+    ElMessage.info('已取消操作')
+    // 取消时也要移除按钮焦点,避免保持激活状态
+    if (button) {
+      button.blur()
+    }
+  })
+}
+
+// 处理分页变化
+const handlePageChange = (page) => {
+  emit('page-change', page)
+}
+
+// 获取当前类型标签
+const getCurrentTypeLabel = () => {
+  const typeMap = {
+    'point': '点',
+    'line': '线',
+    'curve': '弧',
+    'polygon': '面'
+  }
+  return typeMap[activeElementType.value] || ''
+}
+
+// === 导航和急停状态相关方法 ===
+
+// 获取导航状态显示文本
+const getNavigationStatusText = (status) => {
+  const statusMap = {
+    'unknown': '未知',
+    'started': '已启动',
+    'stopped': '已停止'
+  }
+  return statusMap[status] || '未知'
+}
+
+// 获取导航状态样式类
+const getNavigationStatusClass = (status) => {
+  const classMap = {
+    'unknown': 'status-unknown',
+    'started': 'status-started',
+    'stopped': 'status-stopped'
+  }
+  return classMap[status] || 'status-unknown'
+}
+
+// 获取急停状态样式类
+const getEmergencyStopClass = (enabled) => {
+  return enabled ? 'status-emergency' : 'status-normal'
+}
+
+// === 当前导航任务相关方法 ===
+
+// 获取当前导航状态文本
+const getCurrentNavigationStatusText = () => {
+  const statusMap = {
+    'idle': '空闲',
+    'planning': '规划中',
+    'navigating': '导航中',
+    'paused': '已暂停',
+    'arrived': '已到达',
+    'failed': '失败'
+  }
+  return statusMap[props.navigationStatus] || '未知'
+}
+
+// 获取当前导航状态样式类
+const getCurrentNavigationStatusClass = () => {
+  const classMap = {
+    'idle': 'status-idle',
+    'planning': 'status-planning',
+    'navigating': 'status-navigating',
+    'paused': 'status-paused',
+    'arrived': 'status-arrived',
+    'failed': 'status-failed'
+  }
+  return classMap[props.navigationStatus] || ''
+}
+
+// 处理导航暂停/恢复
+const handleNavigationPauseResume = () => {
+  if (props.navigationStatus === 'paused') {
+    emit('navigation-resume')
+  } else if (props.navigationStatus === 'navigating') {
+    emit('navigation-pause')
+  }
+}
+
+// 处理导航停止
+const handleNavigationStop = () => {
+  emit('navigation-stop')
+}
+</script>
+
+<style>
+@import './_map-shared.css';
+
+/* 容器 */
+.right-panel-container {
+  position: relative;
+  height: 100%;
+  z-index: inherit;
+}
+
+/* === 标定页专用样式 === */
+.rp--calib .right-panel {
+  height: 100%;
+  background: #fff;
+  border-radius: 8px;
+  transition: all 0.2s ease;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  position: relative;
+}
+
+.rp--calib .right-panel.panel-collapsed {
+  width: 0 !important;
+  opacity: 0;
+  pointer-events: none;
+  overflow: hidden;
+  transform: translateX(100%);
+}
+
+/* === 标定页样式 === */
+.rp--calib .right-panel {
+  height: 100%;
+  background: #fff;
+  border-radius: 8px;
+  transition: all 0.2s ease;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  position: relative;
+}
+
+.rp--calib .right-panel.panel-collapsed {
+  width: 0 !important;
+  opacity: 0;
+  pointer-events: none;
+  overflow: hidden;
+  transform: translateX(100%);
+}
+
+.rp--calib .panel-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  border-bottom: 1px solid #e4e7ed;
+  background: #fafbfc;
+  flex-shrink: 0;
+}
+
+.rp--calib .panel-title h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.4;
+}
+
+.rp--calib .panel-subtitle {
+  font-size: 12px;
+  color: #f56c6c;
+  margin-left: 8px;
+}
+
+.rp--calib .collapse-btn {
+  color: #909399;
+  padding: 4px;
+}
+
+.rp--calib .collapse-btn:hover {
+  color: #409eff;
+  background-color: #f0f9ff;
+}
+
+.rp--calib .panel-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.rp--calib .default-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  overflow: hidden;
+}
+
+.rp--calib .tab-placeholder {
+  padding: 16px;
+  text-align: center;
+  color: #64748B;
+}
+
+.rp--calib .info-card {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+  margin-bottom: 16px;
+}
+
+.rp--calib .info-card:last-child {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.rp--calib .info-card:last-child .card-content {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.rp--calib .pose-section {
+  margin-bottom: 16px;
+}
+
+.rp--calib .pose-section:last-child {
+  margin-bottom: 0;
+}
+
+.rp--calib .section-title {
+  font-size: 13px;
+  font-weight: 500;
+  color: #64748B;
+  margin-bottom: 8px;
+  padding-bottom: 4px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+}
+
+.rp--calib .pose-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+}
+
+.rp--calib .pose-item:last-child {
+  border-bottom: none;
+}
+
+.rp--calib .pose-label {
+  font-size: 13px;
+  color: #475569;
+  font-weight: 500;
+}
+
+.rp--calib .pose-value {
+  font-size: 13px;
+  color: #0F172A;
+  font-weight: 400;
+  text-align: right;
+}
+
+.rp--calib .calibration-table .table-header,
+.rp--calib .calibration-table .table-row {
+  display: grid;
+  grid-template-columns: 48px 1fr 48px;
+  gap: 8px;
+  align-items: center;
+}
+
+.rp--calib .calibration-table .table-header {
+  padding: 8px 0;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+  font-size: 12px;
+  font-weight: 600;
+  color: #64748B;
+  text-align: center;
+}
+
+.rp--calib .calibration-table .table-row {
+  padding: 8px 0;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+  font-size: 13px;
+}
+
+.rp--calib .calibration-table .table-row:last-child {
+  border-bottom: none;
+}
+
+.rp--calib .calibration-table .table-row:hover {
+  background: #F8FAFC;
+}
+
+.rp--calib .col-id {
+  text-align: center;
+  font-weight: 500;
+  color: #0EA5E9;
+}
+
+.rp--calib .col-coord {
+  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 12px;
+  color: #475569;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.rp--calib .col-action {
+  text-align: center;
+}
+
+.rp--calib .delete-btn {
+  color: #EF4444;
+}
+
+.rp--calib .delete-btn:hover {
+  color: var(--color-danger-dark);
+}
+
+.rp--calib .action-buttons {
+  display: flex;
+  gap: 8px;
+  margin-top: 16px;
+}
+
+.rp--calib .action-buttons .el-button {
+  flex: 1;
+}
+
+.rp--calib .empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 24px 16px;
+  color: #94A3B8;
+}
+
+.rp--calib .empty-state i {
+  font-size: 20px;
+  margin-bottom: 8px;
+  opacity: 0.5;
+}
+
+.rp--calib .empty-state p {
+  margin: 0;
+  font-size: 13px;
+  opacity: 0.8;
+}
+
+.rp--calib .realtime-info-content {
+  padding: 16px;
+  height: 100%;
+  overflow-y: auto;
+}
+
+.rp--calib .info-section {
+  margin-bottom: 20px;
+}
+
+.rp--calib .info-section:last-child {
+  margin-bottom: 0;
+}
+
+.rp--calib .info-title {
+  display: flex;
+  align-items: center;
+  color: #8a8a8a;
+  font-size: 14px;
+  font-weight: 600;
+  border-bottom: 2px solid #B9B9FF;
+  margin-bottom: 12px;
+  padding-bottom: 4px;
+}
+
+.rp--calib .title-bar {
+  height: 20px;
+  width: 5px;
+  background-color: #6565FC;
+  margin-right: 4px;
+  border-radius: 4px 4px 0 0;
+}
+
+.rp--calib .info-content {
+  color: #5A5A5A;
+  font-size: 13px;
+  margin-left: 8px;
+  display: block;
+  margin-bottom: 8px;
+}
+
+.rp--calib .info-details {
+  margin-left: 8px;
+}
+
+.rp--calib .info-item {
+  color: #5A5A5A;
+  font-size: 13px;
+  display: block;
+  margin-bottom: 8px;
+}
+
+.rp--calib .info-label {
+  font-weight: 600;
+}
+
+.rp--calib .info-value {
+  font-weight: 400;
+}
+
+.rp--calib :deep(.el-tabs) {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.rp--calib ::v-deep(.el-tabs__header) {
+  margin: 0;
+  padding: 0 16px !important;
+  background: #fff;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+.rp--calib :deep(.el-tabs__content) {
+  flex: 1;
+  overflow: hidden;
+  padding: 0;
+}
+
+.rp--calib :deep(.el-tab-pane) {
+  height: 100%;
+  overflow-y: auto;
+}
+
+.rp--calib :deep(.el-tab-pane)::-webkit-scrollbar {
+  width: 6px;
+}
+
+.rp--calib :deep(.el-tab-pane)::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.rp--calib :deep(.el-tab-pane)::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.rp--calib :deep(.el-tab-pane)::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.rp--calib .resize-handle {
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: 8px;
+  cursor: col-resize;
+  background: transparent;
+  z-index: 10;
+}
+
+.rp--calib .resize-handle:hover {
+  background: #0EA5E9;
+  opacity: 0.3;
+}
+
+.rp--calib .resize-handle:active {
+  background: #0EA5E9;
+  opacity: 0.5;
+}
+
+.rp--calib .nav-tab-content {
+  height: 100%;
+  padding: 16px;
+  overflow-y: auto;
+}
+
+/* 暗色主题适配 */
+html.dark .right-panel {
+  background: #F1F5F9;
+}
+
+html.dark .panel-header {
+  background: var(--color-bg-quaternary);
+}
+
+html.dark .info-card {
+  background: #F1F5F9;
+  box-shadow: 0 8px 24px rgba(2, 6, 23, 0.06);
+}
+
+html.dark .info-card .card-header {
+  background: var(--color-bg-quaternary);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .expand-handle {
+    width: 32px;
+    height: 60px;
+  }
+}
+
+/* === 导航页 info-card 样式 === */
+.rp--nav .info-card {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+  margin: 0 0 16px;
+}
+
+.rp--nav .card-header {
+  padding: 12px 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+  background: #f7f9fc;
+  border-radius: 8px 8px 0 0;
+}
+
+.rp--nav .card-title {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #0F172A;
+  display: flex;
+  align-items: center;
+}
+
+.rp--nav .card-content {
+  padding: 0;
+}
+
+.rp--nav .pose-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+}
+
+.rp--nav .pose-item:last-child {
+  border-bottom: none;
+}
+
+.rp--nav .pose-label {
+  font-size: 13px;
+  color: #475569;
+  font-weight: 500;
+}
+
+.rp--nav .pose-value {
+  font-size: 13px;
+  color: #0F172A;
+  font-weight: 500;
+  text-align: right;
+  font-family: inherit;
+  letter-spacing: .2px;
+}
+
+/* === 标定页 info-card === */
+.rp--calib .info-card {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+}
+
+.rp--calib .info-card .card-header {
+  padding: 12px 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+  background: #F8FAFC;
+}
+
+.rp--calib .info-card .card-title {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #0F172A;
+}
+
+.rp--calib .info-card .card-content {
+  padding: 0;
+}
+
+.rp--calib .info-card .pose-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+}
+
+.rp--calib .info-card .pose-item:last-child {
+  border-bottom: none;
+}
+
+.rp--calib .info-card .pose-label {
+  font-size: 13px;
+  color: #475569;
+  font-weight: 500;
+}
+
+.rp--calib .info-card .pose-value {
+  font-size: 13px;
+  color: #0F172A;
+  font-weight: 400;
+  text-align: right;
+  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
+}
+
+.rp--calib .info-card :deep(.el-tabs__header) {
+  margin: 0;
+  background: #fff;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+}
+
+.rp--calib .info-card :deep(.el-tabs__item) {
+  font-size: 14px;
+  color: #475569;
+}
+
+.rp--calib .info-card :deep(.el-tabs__item.is-active) {
+  color: #0EA5E9;
+  font-weight: 600;
+}
+
+/* === Tabs 容器高度控制(防止内容被再挤) === */
+.rp--nav .nav-tabs {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.rp--nav :deep(.el-tabs__content) {
+  flex: 1;
+  height: 0;
+  overflow: hidden;
+  padding: 0 !important;
+}
+
+.rp--nav :deep(.el-tab-pane) {
+  height: 100%;
+  overflow: auto;
+}
+
+.expand-handle {
+  position: fixed;
+  right: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  z-index: 12;
+  width: 36px;
+  height: 72px;
+  background: #FFFFFF;
+  border: 1px solid rgba(2, 6, 23, 0.08);
+  border-radius: 8px 0 0 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #475569;
+  transition: all var(--transition-duration, 0.2s) var(--animation-ease, ease-out);
+  pointer-events: auto;
+}
+
+.expand-handle:hover {
+  background: #0EA5E9;
+  color: var(--color-text-inverse);
+  border-color: #0EA5E9;
+  transform: translateY(-50%) translateX(-2px);
+  box-shadow: 0 20px 25px -5px rgba(2, 6, 23, 0.1), 0 10px 10px -5px rgba(2, 6, 23, 0.04);;
+}
+
+.expand-handle i {
+  font-size: 16px;
+  font-weight: bold;
+}
+
+/* 暗色主题适配 */
+html.dark .right-panel {
+  background: #F1F5F9;
+}
+
+html.dark .panel-header {
+  background: var(--color-bg-quaternary);
+}
+
+html.dark .info-card {
+  background: #F1F5F9;
+  box-shadow: 0 8px 24px rgba(2, 6, 23, 0.06);
+}
+
+html.dark .info-card .card-header {
+  background: var(--color-bg-quaternary);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .expand-handle {
+    width: 32px;
+    height: 60px;
+  }
+}
+
+/* === 导航页专用样式 === */
+.rp--nav .nav-expand-handle:hover {
+  background: var(--color-primary, #409eff) !important;
+  border-color: var(--color-primary, #409eff) !important;
+  transform: translateY(-50%) translateX(-2px) !important;
+}
+
+.rp--nav .nav-expand-handle:hover i {
+  color: #ffffff !important;
+}
+
+.rp--nav .nav-right-panel {
+  position: fixed;
+  right: 16px;
+  top: 100px;
+  width: 380px;
+  height: calc(100vh - 116px);
+  z-index: 1000;
+  transition: transform 0.3s ease;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.rp--nav .nav-right-panel.is-hidden {
+  transform: translateX(calc(100% + 16px));
+}
+
+.rp--nav .nav-panel-header {
+  position: relative;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.rp--nav .nav-panel-close-btn {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 10;
+  padding: 4px;
+  font-size: 14px;
+  color: #909399;
+}
+
+.rp--nav .nav-panel-close-btn:hover {
+  color: #409eff;
+  background: rgba(64, 158, 255, 0.1);
+}
+
+.rp--nav .nav-tabs {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.rp--nav .nav-tabs :deep(.el-tabs__header) {
+  margin: 0;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+  background: #F8FAFC;
+  flex-shrink: 0;
+}
+
+.rp--nav .nav-tabs :deep(.el-tabs__nav-wrap) {
+  padding: 0 16px;
+}
+
+.rp--nav .nav-tabs :deep(.el-tabs__item) {
+  padding: 0 16px;
+  font-size: 14px;
+  color: #475569;
+  line-height: 40px;
+}
+
+.rp--nav .nav-tabs :deep(.el-tabs__item.is-active) {
+  color: #0EA5E9;
+  font-weight: 600;
+}
+
+.rp--nav .nav-tabs :deep(.el-tabs__content) {
+  flex: 1;
+  height: 0;
+  overflow: hidden;
+}
+
+.rp--nav .nav-tabs :deep(.el-tab-pane) {
+  height: 100%;
+  overflow: auto;
+}
+
+.rp--nav .nav-tabs :deep(.el-tab-pane)::-webkit-scrollbar {
+  width: 6px;
+}
+
+.rp--nav .nav-tabs :deep(.el-tab-pane)::-webkit-scrollbar-track {
+  background: #F1F5F9;
+  border-radius: 9999px;
+}
+
+.rp--nav .nav-tabs :deep(.el-tab-pane)::-webkit-scrollbar-thumb {
+  background: rgba(2, 6, 23, 0.08);
+  border-radius: 9999px;
+}
+
+.rp--nav .nav-tabs :deep(.el-tab-pane)::-webkit-scrollbar-thumb:hover {
+  background: #94A3B8;
+}
+
+.rp--nav .nav-tab-content {
+  height: 100%;
+  padding: 0;
+  overflow-y: auto;
+}
+
+.rp--nav .nav-info-grid {
+  background: #FFFFFF;
+  border-radius: 8px;
+  box-shadow: 0 8px 24px rgba(2, 6, 23, 0.06);
+  overflow: hidden;
+  margin-bottom: 0;
+}
+
+.rp--nav .nav-info-header {
+  padding: 12px 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+  background: #F8FAFC;
+}
+
+.rp--nav .nav-info-title {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #0F172A;
+}
+
+.rp--nav .nav-info-content {
+  padding: 0;
+}
+
+.rp--nav .nav-info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+}
+
+.rp--nav .nav-info-item:last-child {
+  border-bottom: none;
+}
+
+.rp--nav .nav-info-item .label {
+  font-size: 13px;
+  color: #475569;
+  font-weight: 400;
+}
+
+.rp--nav .nav-info-item .value {
+  font-size: 13px;
+  color: #0F172A;
+  text-align: right;
+  font-weight: 500;
+  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
+}
+
+/* 电池显示样式 */
+.rp--nav .battery-display {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.rp--nav .battery-percentage {
+  font-weight: 600;
+}
+
+.rp--nav .battery-percentage.battery-normal {
+  color: #10b981;
+}
+
+.rp--nav .battery-percentage.battery-medium {
+  color: #f59e0b;
+}
+
+.rp--nav .battery-percentage.battery-low {
+  color: #ef4444;
+  animation: blink 1s infinite;
+}
+
+.rp--nav .battery-percentage.battery-charging {
+  color: #10b981;
+}
+
+.rp--nav .charging-icon {
+  font-size: 14px;
+  animation: pulse 1s infinite;
+}
+
+.rp--nav .charging-text {
+  color: #10b981;
+  font-weight: 600;
+}
+
+.rp--nav .battery-detail {
+  background: #f8fafc;
+  padding-left: 32px;
+}
+
+.rp--nav .battery-detail .label {
+  font-size: 12px;
+  color: #64748b;
+}
+
+.rp--nav .battery-detail .value {
+  font-size: 12px;
+}
+
+@keyframes blink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+@keyframes pulse {
+  0%, 100% { transform: scale(1); }
+  50% { transform: scale(1.2); }
+}
+
+.rp--nav .nav-list-header {
+  margin-bottom: 16px;
+}
+
+.rp--nav .nav-list-header .el-input {
+  margin-bottom: 12px;
+}
+
+.rp--nav .nav-header-buttons {
+  display: flex;
+  gap: 8px;
+}
+
+.rp--nav .nav-header-buttons .el-button {
+  flex: 1;
+}
+
+.rp--nav .nav-list-content {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.rp--nav .nav-list-content.nav-list-scrollable {
+  max-height: calc(100vh - 400px);
+}
+
+.rp--nav .nav-list-content.nav-list-scrollable::-webkit-scrollbar {
+  width: 6px;
+}
+
+.rp--nav .nav-list-content.nav-list-scrollable::-webkit-scrollbar-track {
+  background: #F1F5F9;
+  border-radius: 9999px;
+}
+
+.rp--nav .nav-list-content.nav-list-scrollable::-webkit-scrollbar-thumb {
+  background: rgba(2, 6, 23, 0.08);
+  border-radius: 9999px;
+}
+
+.rp--nav .nav-list-content.nav-list-scrollable::-webkit-scrollbar-thumb:hover {
+  background: #94A3B8;
+}
+
+.rp--nav .nav-waypoint-list .nav-waypoint-item {
+  padding: 12px;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  margin-bottom: 8px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  display: flex;
+  align-items: center;
+}
+
+.rp--nav .nav-waypoint-item:hover {
+  border-color: #409eff;
+  background: #f0f9ff;
+}
+
+.rp--nav .nav-waypoint-item.selected {
+  border-color: #409eff;
+  background: #f0f9ff;
+}
+
+.rp--nav .waypoint-checkbox {
+  flex-shrink: 0;
+  margin-right: 8px;
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+  padding: 8px !important;
+  cursor: pointer !important;
+  border-radius: 4px !important;
+  transition: background-color 0.2s ease !important;
+  position: relative !important;
+  z-index: 10 !important;
+  min-width: 32px !important;
+  min-height: 32px !important;
+}
+
+.rp--nav .waypoint-checkbox:hover {
+  background-color: rgba(64, 158, 255, 0.1) !important;
+}
+
+.rp--nav .waypoint-checkbox:active {
+  background-color: rgba(64, 158, 255, 0.2) !important;
+  transform: scale(0.98) !important;
+}
+
+.rp--nav .custom-checkbox {
+  width: 16px !important;
+  height: 16px !important;
+  border: 2px solid #dcdfe6 !important;
+  border-radius: 3px !important;
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+  cursor: pointer !important;
+  transition: all 0.2s ease !important;
+  background: #fff !important;
+  position: relative !important;
+}
+
+.rp--nav .custom-checkbox:hover {
+  border-color: #409eff !important;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1) !important;
+}
+
+.rp--nav .custom-checkbox:active {
+  transform: scale(0.95) !important;
+}
+
+.rp--nav .custom-checkbox.checked {
+  background-color: #409eff !important;
+  border-color: #409eff !important;
+}
+
+.rp--nav .waypoint-info {
+  flex: 1;
+}
+
+.rp--nav .waypoint-main {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.rp--nav .waypoint-id {
+  font-weight: 500;
+  color: #333;
+  font-size: 14px;
+}
+
+.rp--nav .waypoint-coords {
+  font-size: 12px;
+  color: #666;
+  font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
+}
+
+.rp--nav .waypoint-actions {
+  display: flex !important;
+  gap: 4px;
+  align-items: baseline !important;
+  flex-shrink: 0;
+}
+
+.rp--nav .action-btn {
+  padding: 2px 6px !important;
+  font-size: 12px !important;
+  height: 24px !important;
+  line-height: 20px !important;
+}
+
+.rp--nav .nav-task-list .nav-task-item {
+  padding: 12px;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  margin-bottom: 8px;
+}
+
+.rp--nav .task-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.rp--nav .task-name {
+  font-weight: 500;
+  color: #333;
+}
+
+.rp--nav .task-status {
+  display: inline-block;
+  padding: 4px 12px;
+  border-radius: 16px;
+  font-size: 12px;
+  font-weight: 600;
+  text-align: center;
+  min-width: 60px;
+  line-height: 1.2;
+  border: 1px solid transparent;
+}
+
+.rp--nav .task-status.status-idle {
+  background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+  color: #0369a1;
+  border-color: #bae6fd;
+}
+
+.rp--nav .task-status.status-running {
+  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+  color: #15803d;
+  border-color: #bbf7d0;
+}
+
+.rp--nav .task-status.status-paused {
+  background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
+  color: #d97706;
+  border-color: #fed7aa;
+}
+
+.rp--nav .task-status.status-completed {
+  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+  color: #15803d;
+  border-color: #bbf7d0;
+}
+
+.rp--nav .task-status.status-failed {
+  background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
+  color: #dc2626;
+  border-color: #fecaca;
+}
+
+.rp--nav .task-status.status-unknown {
+  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+  color: #64748b;
+  border-color: #cbd5e1;
+}
+
+.rp--nav .task-status.status-started {
+  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+  color: #15803d;
+  border-color: #bbf7d0;
+}
+
+.rp--nav .task-status.status-stopped {
+  background: linear-gradient(135deg, #fefbf3 0%, #fef3c7 100%);
+  color: #d97706;
+  border-color: #fed7aa;
+}
+
+.rp--nav .task-status.status-emergency {
+  background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
+  color: #dc2626;
+  border-color: #fecaca;
+  animation: pulse-emergency 2s infinite;
+}
+
+.rp--nav .task-status.status-planning {
+  background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
+  color: #d97706;
+  border-color: #fed7aa;
+}
+
+.rp--nav .task-status.status-navigating {
+  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+  color: #15803d;
+  border-color: #bbf7d0;
+  animation: pulse-navigating 2s infinite;
+}
+
+.rp--nav .task-status.status-arrived {
+  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+  color: #15803d;
+  border-color: #bbf7d0;
+}
+
+.rp--nav .task-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.rp--nav .task-actions .el-button {
+  padding: 2px 6px;
+}
+
+.rp--nav .nav-empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  color: #999;
+}
+
+.rp--nav .nav-empty-state i {
+  font-size: 48px;
+  margin-bottom: 12px;
+  opacity: 0.5;
+}
+
+.rp--nav .nav-empty-state p {
+  margin: 0;
+  font-size: 14px;
+}
+
+.rp--nav .settings-list {
+  padding: 0;
+}
+
+.rp--nav .setting-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.rp--nav .setting-item:last-child {
+  border-bottom: none;
+}
+
+.rp--nav .setting-info {
+  flex: 1;
+}
+
+.rp--nav .setting-label {
+  display: block;
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 4px;
+}
+
+.rp--nav .setting-desc {
+  display: block;
+  font-size: 12px;
+  color: #666;
+  line-height: 1.4;
+}
+
+.rp--nav .setting-item .el-switch {
+  margin-left: 16px;
+}
+
+.rp--nav .waypoint-toolbar {
+  background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  padding: 14px;
+  margin-bottom: 16px;
+  box-shadow: 0 8px 24px rgba(2, 6, 23, 0.06);
+}
+
+.rp--nav .toolbar-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  grid-template-rows: repeat(2, auto);
+  gap: 12px;
+  width: 100%;
+}
+
+@media (max-width: 768px) {
+  .rp--nav .toolbar-grid {
+    grid-template-columns: 1fr;
+    gap: 10px;
+  }
+}
+
+.rp--nav .grid-btn {
+  width: 100% !important;
+  height: 44px !important;
+  min-width: 0 !important;
+  max-width: 100% !important;
+  font-size: 12px !important;
+  font-weight: 600 !important;
+  padding: 8px 6px !important;
+  margin: 0 !important;
+  border-radius: 8px !important;
+  border-width: 1px !important;
+  display: flex !important;
+  flex-direction: column !important;
+  align-items: center !important;
+  justify-content: center !important;
+  gap: 3px !important;
+  text-align: center !important;
+  transition: all 0.2s ease !important;
+  box-sizing: border-box !important;
+  flex-shrink: 0 !important;
+  flex-grow: 0 !important;
+}
+
+.rp--nav .grid-btn i {
+  font-size: 15px !important;
+  line-height: 1 !important;
+}
+
+.rp--nav .grid-btn span {
+  font-size: 11px !important;
+  line-height: 1.2 !important;
+  font-weight: 600 !important;
+  white-space: nowrap !important;
+  overflow: hidden !important;
+  text-overflow: ellipsis !important;
+}
+
+.rp--nav .grid-btn.el-button--primary {
+  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
+  border: 1px solid #2563eb !important;
+  color: white !important;
+}
+
+.rp--nav .grid-btn.el-button--primary:hover:not(:disabled) {
+  background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
+}
+
+.rp--nav .grid-btn.el-button--danger {
+  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
+  border: 1px solid #dc2626 !important;
+  color: white !important;
+}
+
+.rp--nav .grid-btn.el-button--warning {
+  background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important;
+  border: 1px solid #d97706 !important;
+  color: white !important;
+}
+
+.rp--nav .grid-btn.el-button--success {
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
+  border: 1px solid #059669 !important;
+  color: white !important;
+}
+
+.rp--nav .grid-btn.el-button--info {
+  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
+  border: 1px solid #2563eb !important;
+  color: white !important;
+}
+
+.rp--nav .grid-btn.is-disabled {
+  opacity: 0.5 !important;
+  cursor: not-allowed !important;
+}
+
+.rp--nav .waypoint-quick-actions {
+  position: sticky;
+  bottom: 0;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  padding: 12px;
+  border-radius: 8px;
+  margin-top: 12px;
+}
+
+.rp--nav .quick-actions-info {
+  font-size: 13px;
+  margin-bottom: 8px;
+  font-weight: 500;
+}
+
+.rp--nav .quick-actions-buttons {
+  display: flex;
+  gap: 8px;
+}
+
+.rp--nav .quick-actions-buttons .el-button {
+  flex: 1;
+  border: 1px solid rgba(255, 255, 255, 0.3);
+  background: rgba(255, 255, 255, 0.1);
+  color: white;
+}
+
+.rp--nav .quick-actions-buttons .el-button:hover {
+  background: rgba(255, 255, 255, 0.2);
+  border-color: rgba(255, 255, 255, 0.5);
+}
+
+.rp--nav .empty-hint {
+  font-size: 12px;
+  color: #999;
+  margin-top: 8px;
+}
+
+/* === 编辑页专用样式 === */
+.rp--edit .right-panel {
+  position: fixed;
+  right: 16px;
+  top: 100px;
+  width: 380px;
+  height: calc(100vh - 116px);
+  z-index: 1000;
+  transition: transform 0.3s ease;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.rp--edit .right-panel.is-hidden {
+  transform: translateX(calc(100% + 16px));
+}
+
+.rp--edit .edit-panel-close-btn {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 10;
+  padding: 4px;
+  font-size: 14px;
+  color: #909399;
+}
+
+.rp--edit .edit-panel-close-btn:hover {
+  color: #409eff;
+  background: rgba(64, 158, 255, 0.1);
+}
+
+.rp--edit .panel-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow-y: auto !important;
+  overflow-x: hidden;
+}
+
+.rp--edit :deep(.el-tabs) {
+  display: block;
+  height: auto !important;
+}
+
+.rp--edit :deep(.el-tabs__header) {
+  margin: 0;
+  padding: 0 16px !important;
+  background: #fff;
+  border-bottom: none;
+}
+
+.rp--edit :deep(.el-tabs__nav-wrap) {
+  overflow: visible;
+}
+
+.rp--edit :deep(.el-tabs__content) {
+  overflow-y: auto !important;
+  overflow-x: hidden;
+  height: auto !important;
+  max-height: none !important;
+}
+
+.rp--edit :deep(.el-tabs__content) > div {
+  overflow: visible !important;
+  display: block;
+}
+
+.rp--edit :deep(.el-tab-pane) {
+  display: block;
+}
+
+.rp--edit :deep(.el-tabs__content),
+.rp--edit .panel-content {
+  scrollbar-width: thin;
+  scrollbar-color: #c1c1c1 #f1f1f1;
+}
+
+.rp--edit :deep(.el-tabs__content)::-webkit-scrollbar,
+.rp--edit .panel-content::-webkit-scrollbar {
+  width: 6px;
+}
+
+.rp--edit :deep(.el-tabs__content)::-webkit-scrollbar-track,
+.rp--edit .panel-content::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.rp--edit :deep(.el-tabs__content)::-webkit-scrollbar-thumb,
+.rp--edit .panel-content::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.rp--edit :deep(.el-tabs__content)::-webkit-scrollbar-thumb:hover,
+.rp--edit .panel-content::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.rp--edit .nav-tab-content {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 0 16px 16px;
+}
+
+.rp--edit .nav-info-grid {
+  background: #FFFFFF;
+  border-radius: var(--panel-border-radius, 8px);
+  box-shadow: 0 8px 24px rgba(2, 6, 23, 0.06);
+  overflow: hidden;
+  margin-bottom: 0;
+}
+
+.rp--edit .nav-info-header {
+  padding: 12px 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.06);
+  background: #F8FAFC;
+}
+
+.rp--edit .nav-info-title {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #0F172A;
+}
+
+.rp--edit .nav-info-content {
+  padding: 0;
+}
+
+.rp--edit .nav-info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  height: 36px;
+  padding: 0 16px;
+  border-bottom: 1px solid rgba(2, 6, 23, 0.04);
+}
+
+.rp--edit .nav-info-item:last-child {
+  border-bottom: none;
+}
+
+.rp--edit .nav-info-item .label {
+  font-size: 13px;
+  color: #475569;
+  font-weight: 400;
+}
+
+.rp--edit .nav-info-item .value {
+  font-size: 13px;
+  color: #0F172A;
+  text-align: right;
+  font-weight: 500;
+  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
+}
+
+.rp--edit .edit-elements-content {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.rp--edit .element-filter {
+  flex-shrink: 0;
+  margin-bottom: 12px;
+}
+
+.rp--edit .element-filter :deep(.el-tabs__header) {
+  margin: 0;
+  background: #F8FAFC;
+  border-bottom: none;
+}
+
+.rp--edit .element-filter :deep(.el-tabs__nav-wrap) {
+  overflow: visible;
+}
+
+.rp--edit .element-search {
+  flex-shrink: 0;
+  margin-bottom: 12px;
+  padding: 0 16px;
+}
+
+.rp--edit .element-list {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 0 16px;
+}
+
+.rp--edit .element-list::-webkit-scrollbar {
+  width: 6px;
+}
+
+.rp--edit .element-list::-webkit-scrollbar-track {
+  background: #F1F5F9;
+  border-radius: 9999px;
+}
+
+.rp--edit .element-list::-webkit-scrollbar-thumb {
+  background: rgba(2, 6, 23, 0.08);
+  border-radius: 9999px;
+}
+
+.rp--edit .element-list::-webkit-scrollbar-thumb:hover {
+  background: #94A3B8;
+}
+
+.rp--edit .element-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+  border: 1px solid rgba(2, 6, 23, 0.04);
+  border-radius: 8px;
+  margin-bottom: 8px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.rp--edit .element-item:hover {
+  border-color: #0EA5E9;
+  background: #F8FAFC;
+}
+
+.rp--edit .element-item.selected {
+  border-color: #0EA5E9;
+  background: #38BDF8;
+}
+
+.rp--edit .element-info {
+  flex: 1;
+}
+
+.rp--edit .element-id {
+  font-weight: 600;
+  color: #0F172A;
+  font-size: 13px;
+  margin-bottom: 4px;
+}
+
+.rp--edit .element-name {
+  color: #475569;
+  font-size: 12px;
+}
+
+.rp--edit .element-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.rp--edit .empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 32px 16px;
+  color: #94A3B8;
+}
+
+.rp--edit .empty-state i {
+  font-size: 20px;
+  margin-bottom: 8px;
+  opacity: 0.5;
+}
+
+.rp--edit .empty-state p {
+  margin: 0;
+  font-size: 13px;
+  opacity: 0.8;
+}
+
+.rp--edit .edit-network-content {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 0 16px 16px;
+}
+
+.rp--edit .edit-network-content::-webkit-scrollbar {
+  width: 6px;
+}
+
+.rp--edit .edit-network-content::-webkit-scrollbar-track {
+  background: #F1F5F9;
+  border-radius: 9999px;
+}
+
+.rp--edit .edit-network-content::-webkit-scrollbar-thumb {
+  background: rgba(2, 6, 23, 0.08);
+  border-radius: 9999px;
+}
+
+.rp--edit .edit-network-content::-webkit-scrollbar-thumb:hover {
+  background: #94A3B8;
+}
+
+.rp--edit .network-operations .operation-section {
+  margin-bottom: 24px;
+}
+
+.rp--edit .operation-section:last-child {
+  margin-bottom: 0;
+}
+
+.rp--edit .section-title {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #0F172A;
+}
+
+.rp--edit .operation-tips {
+  background: #F8FAFC;
+  padding: 16px;
+  border-radius: 8px;
+}
+
+.rp--edit .tip-item {
+  margin: 0 0 12px 0;
+  font-size: 13px;
+  line-height: 1.6;
+  color: #475569;
+}
+
+.rp--edit .tip-item:last-child {
+  margin-bottom: 0;
+}
+
+.rp--edit .tip-item strong {
+  color: #0F172A;
+  font-weight: 600;
+  margin-right: 4px;
+}
+
+.rp--edit .operation-buttons {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.rp--edit .operation-buttons .el-button {
+  margin: 0;
+  transition: all 0.2s ease;
+}
+
+.rp--edit .operation-buttons .el-button:not(:hover):not(:active) {
+  background-color: var(--color-bg-button, #ffffff);
+  border-color: var(--color-border-primary, #dcdfe6);
+  color: var(--color-text-primary, #606266);
+}
+
+.rp--edit .operation-buttons .el-button:not(:hover):not(:active).el-button--primary {
+  background-color: var(--color-primary, #409eff);
+  border-color: var(--color-primary, #409eff);
+  color: #ffffff;
+}
+
+.rp--edit .operation-buttons .el-button:focus {
+  outline: none;
+}
+
+.rp--edit .operation-buttons .el-button:active {
+  transform: translateY(1px);
+}
+
+/* // 急停状态脉冲动画 */
+@keyframes pulse-emergency {
+  0% {
+    box-shadow: 0 1px 3px rgba(220, 38, 38, 0.1), 0 0 0 0 rgba(220, 38, 38, 0.4);
+  }
+  50% {
+    box-shadow: 0 1px 3px rgba(220, 38, 38, 0.2), 0 0 0 6px rgba(220, 38, 38, 0.1);
+  }
+  100% {
+    box-shadow: 0 1px 3px rgba(220, 38, 38, 0.1), 0 0 0 0 rgba(220, 38, 38, 0);
+  }
+}
+
+/* 动画 */
+@keyframes pulse-emergency {
+  0% {
+    box-shadow: 0 1px 3px rgba(220, 38, 38, 0.1), 0 0 0 0 rgba(220, 38, 38, 0.4);
+  }
+  50% {
+    box-shadow: 0 1px 3px rgba(220, 38, 38, 0.2), 0 0 0 6px rgba(220, 38, 38, 0.1);
+  }
+  100% {
+    box-shadow: 0 1px 3px rgba(220, 38, 38, 0.1), 0 0 0 0 rgba(220, 38, 38, 0);
+  }
+}
+
+@keyframes pulse-navigating {
+  0% {
+    box-shadow: 0 1px 3px rgba(21, 128, 61, 0.1), 0 0 0 0 rgba(21, 128, 61, 0.4);
+  }
+  50% {
+    box-shadow: 0 1px 3px rgba(21, 128, 61, 0.2), 0 0 0 6px rgba(21, 128, 61, 0.1);
+  }
+  100% {
+    box-shadow: 0 1px 3px rgba(21, 128, 61, 0.1), 0 0 0 0 rgba(21, 128, 61, 0);
+  }
+}
+</style>

+ 87 - 0
src/views/map/components/shared/_map-shared.css

@@ -0,0 +1,87 @@
+/* 地图组件共用 CSS 变量 - Vue 3 纯 CSS 版本 */
+
+/* Z-Index层级 */
+:root {
+  --map-z-index-base: 1;
+  --map-z-index-toolbar: 11;
+  --map-z-index-panel: 10;
+  --map-z-index-expand-btn: 12;
+
+  /* 尺寸规格 */
+  --toolbar-width: 60px;
+  --toolbar-btn-size: 36px;
+  --toolbar-btn-size-sm: 32px;
+  --panel-width-default: 360px;
+  --panel-width-min: 320px;
+  --panel-width-max: 420px;
+  --resize-handle-width: 8px;
+
+  /* 间距 */
+  --toolbar-gap: 8px;
+  --toolbar-padding: 12px;
+  --panel-padding: 16px;
+  --card-margin-bottom: 16px;
+
+  /* 圆角 */
+  --toolbar-border-radius: 8px;
+  --panel-border-radius: 8px;
+  --btn-border-radius: 50%;
+
+  /* 阴影 */
+  --toolbar-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
+  --panel-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
+  --btn-hover-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+
+  /* 动画时长 */
+  --transition-duration: 0.2s;
+  --animation-ease: ease-out;
+
+  /* 通用间距变量 */
+  --spacing-1: 4px;
+  --spacing-2: 8px;
+  --spacing-3: 12px;
+  --spacing-4: 16px;
+  --spacing-5: 20px;
+  --spacing-6: 24px;
+  --spacing-8: 32px;
+}
+
+/* 暗色主题变量 */
+html.dark {
+  --map-z-index-base: 1;
+  --map-z-index-toolbar: 11;
+  --map-z-index-panel: 10;
+  --map-z-index-expand-btn: 12;
+
+  --toolbar-width: 60px;
+  --toolbar-btn-size: 36px;
+  --toolbar-btn-size-sm: 32px;
+  --panel-width-default: 360px;
+  --panel-width-min: 320px;
+  --panel-width-max: 420px;
+  --resize-handle-width: 8px;
+
+  --toolbar-gap: 8px;
+  --toolbar-padding: 12px;
+  --panel-padding: 16px;
+  --card-margin-bottom: 16px;
+
+  --toolbar-border-radius: 8px;
+  --panel-border-radius: 8px;
+  --btn-border-radius: 50%;
+
+  --toolbar-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
+  --panel-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
+  --btn-hover-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
+
+  --transition-duration: 0.2s;
+  --animation-ease: ease-out;
+
+  --spacing-1: 4px;
+  --spacing-2: 8px;
+  --spacing-3: 12px;
+  --spacing-4: 16px;
+  --spacing-5: 20px;
+  --spacing-6: 24px;
+  --spacing-8: 32px;
+}

+ 4 - 0
src/views/map/components/shared/index.js

@@ -0,0 +1,4 @@
+import MapToolbar from './MapToolbar.vue'
+import RightPanel from './RightPanel.vue'
+
+export { MapToolbar, RightPanel }

+ 933 - 0
src/views/map/maplist/calibration.vue

@@ -0,0 +1,933 @@
+<template>
+  <div class="calibration-container">
+    <!-- 地图舞台容器 -->
+    <div class="map-stage" ref="mapStageRef">
+      <!-- 地图底层 -->
+      <div class="map-canvas-wrapper">
+        <OlMap 
+          ref="olmapRef" 
+          :width="olWidth + 'px'" 
+          :height="olHeight + 'px'" 
+          backgroundColor="#F5F5F5"
+          :robotPoseData="laserPositionData" 
+          :poseCalibrationIndex="nowCalibId"
+          :showDefaultControls="false"
+          :mapName="mapName"
+        />
+      </div>
+
+      <!-- 地图工具条 -->
+      <MapToolbar 
+        class="map-toolbar"
+        preset="calibration"
+        :canAddCalibration="canAddCalibration"
+        :hasRobotPosition="hasRobotPosition"
+        :isFullscreen="isFullscreen"
+        :isConnected="wsConnected"
+        :isBusy="false"
+        @zoom-in="handleZoomIn"
+        @zoom-out="handleZoomOut"
+        @center-robot="handleCenterToRobot"
+        @toggle-fullscreen="handleToggleFullscreen"
+        @add-calibration-point="addCalibration"
+      />
+
+      <!-- 右侧信息面板 (宽屏模式,浮层) -->
+      <RightPanel 
+        v-if="!isMobileMode"
+        mode="calib"
+        class="right-panel"
+        v-model="isPanelVisible"
+        :width="rightPanelWidth"
+        :title="'实时标定信息'"
+        :subtitle="`(当前地图: ${currentMap.name})`"
+        :laserPositionData="laserPositionData"
+        :gnssPositionData="gnssPositionData"
+        :calibrationList="calibrationList"
+        :currentMap="currentMap"
+        :panelWidth="rightPanelWidth"
+        :mapName="mapName"
+        @panel-resize="handlePanelResize"
+        @add-calibration="addCalibration"
+        @remove-calibration="removeCalibration"
+        @execute-calibration="executeCalibration"
+      />
+
+      <!-- 右上角信息按钮 (窄屏模式) -->
+      <div class="info-toggle-btn" v-if="isMobileMode" @click="showInfoDrawer">
+        <el-button type="primary" :icon="InfoFilled" circle size="small" />
+      </div>
+    </div>
+
+    <!-- 抽屉模式 (窄屏) -->
+    <el-drawer
+      v-model="drawerVisible"
+      direction="rtl"
+      :modal="false"
+      size="360px"
+      :with-header="false"
+      custom-class="info-drawer"
+      :wrapperClosable="false"
+      v-if="isMobileMode"
+    >
+      <div class="drawer-content">
+        <div class="drawer-header">
+          <div class="drawer-title">
+            <h3>实时标定信息</h3>
+            <span class="map-name">(当前地图: {{ currentMap.name }})</span>
+          </div>
+          <el-button 
+            @click="drawerVisible = false" 
+            type="text" 
+            size="small" 
+            :icon="Close"
+          />
+        </div>
+        
+        <div class="drawer-body">
+          <RightPanel 
+            :laserPositionData="laserPositionData"
+            :gnssPositionData="gnssPositionData"
+            :calibrationList="calibrationList"
+            :currentMap="currentMap"
+            :isDrawerMode="true"
+            @add-calibration="addCalibration"
+            @remove-calibration="removeCalibration"
+            @execute-calibration="executeCalibration"
+            class="drawer-panel"
+          />
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { InfoFilled, Close } from '@element-plus/icons-vue'
+import OlMap from "@/components/OlMap"
+import MapToolbar from "../components/shared/MapToolbar.vue"
+import RightPanel from "../components/shared/RightPanel.vue"
+import { getCalibrationHistory } from "@/api/robot/map"
+import { initWebSocket, disconnect, subscribe, unsubscribe } from "@/utils/websocket"
+import { sendMqttMessage } from "@/api/robot/mqtt"
+
+defineOptions({
+  name: 'CalibrationPage'
+})
+
+// === Route ===
+const route = useRoute()
+
+// === Template Refs ===
+const mapStageRef = ref(null)
+const olmapRef = ref(null)
+
+// === Reactive State ===
+const mapName = ref(route.query.mapName || 'Unknown')
+
+const topics = ref([
+  '/localization/pose',
+  '/settings/multi_coordinates/action/calibrate/reply'
+])
+
+const calibrationList = ref([])
+
+const laserPositionData = reactive({
+  x: 0,
+  y: 0,
+  angle: 0
+})
+
+const gnssPositionData = reactive({
+  status: '0/0',
+  longitude: 0,
+  latitude: 0,
+  angle: 0,
+  satellites: 0
+})
+
+const currentMap = reactive({
+  name: route.query.mapName || 'Unknown'
+})
+
+const olWidth = ref(0)
+const olHeight = ref(0)
+const nowCalibId = ref(0)
+
+const rightPanelWidth = ref(360)
+const isPanelVisible = ref(true)
+const windowWidth = ref(window.innerWidth)
+const drawerVisible = ref(false)
+const isFullscreen = ref(false)
+
+const wsConnected = ref(false)
+const wsSubscriptions = ref([])
+
+// === Computed ===
+const isMobileMode = computed(() => {
+  return windowWidth.value < 1440
+})
+
+const canAddCalibration = computed(() => {
+  return laserPositionData.x !== 0 || laserPositionData.y !== 0
+})
+
+const hasRobotPosition = computed(() => {
+  return laserPositionData.x !== 0 || laserPositionData.y !== 0
+})
+
+// === WebSocket ===
+const setupWebSocket = () => {
+  initWebSocket({
+    onConnect: () => {
+      wsConnected.value = true
+      console.log('WebSocket连接成功')
+    },
+    onDisconnect: () => {
+      wsConnected.value = false
+      console.log('WebSocket连接断开')
+    },
+    onMessage: (type, message) => {
+      handleWsMessage(type, message)
+    },
+    onError: (error) => {
+      console.error('WebSocket错误:', error)
+    }
+  })
+}
+
+const handleWsMessage = (type, message) => {
+  try {
+    // 处理位姿数据
+    if (type === 'pose') {
+      const data = message.data || message
+      const { xyz, rpy, blh, heading } = data.pose || data
+      
+      if (xyz) {
+        laserPositionData.x = xyz[0] || 0
+        laserPositionData.y = xyz[1] || 0
+        laserPositionData.angle = rpy ? rpy[2] : 0
+      }
+      
+      if (blh) {
+        gnssPositionData.longitude = blh[1] || 0
+        gnssPositionData.latitude = blh[0] || 0
+        gnssPositionData.angle = heading || 0
+      }
+      
+      if (data.rtk) {
+        gnssPositionData.satellites = data.rtk.star || 0
+        gnssPositionData.status = `${data.rtk.star || 0}/${data.rtk.status || 0}`
+      }
+    }
+    // 处理标定响应
+    else if (type === 'raw' && message.topic && message.topic.includes('/calibrate/reply')) {
+      const payload = message.payload || message
+      if (payload.status === 'ok') {
+        ElMessage.success('标定执行成功')
+      } else {
+        ElMessage.error('标定执行失败: ' + (payload.error || '未知错误'))
+      }
+    }
+  } catch (e) {
+    console.error('消息解析失败:', e)
+  }
+}
+
+// === Map Methods ===
+const updateOlCss = () => {
+  nextTick(() => {
+    const mapStage = mapStageRef.value
+    if (mapStage) {
+      olWidth.value = mapStage.offsetWidth
+      olHeight.value = mapStage.offsetHeight
+      
+      if (olmapRef.value?.map) {
+        olmapRef.value.map.updateSize()
+      }
+    }
+  })
+}
+
+const handleWindowResize = () => {
+  windowWidth.value = window.innerWidth
+  updateOlCss()
+}
+
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!(
+    document.fullscreenElement ||
+    document.webkitFullscreenElement ||
+    document.mozFullScreenElement ||
+    document.msFullscreenElement
+  )
+  
+  setTimeout(() => {
+    updateOlCss()
+  }, 100)
+}
+
+const updateRightPanelWidth = () => {
+  if (isMobileMode.value) {
+    isPanelVisible.value = false
+  } else {
+    const savedWidth = localStorage.getItem('calibration-panel-width')
+    const savedCollapsed = localStorage.getItem('calibration-panel-collapsed')
+    
+    if (savedCollapsed === 'true') {
+      isPanelVisible.value = false
+      rightPanelWidth.value = 360
+    } else if (savedWidth) {
+      rightPanelWidth.value = Math.max(320, Math.min(420, parseInt(savedWidth)))
+      isPanelVisible.value = true
+    } else {
+      rightPanelWidth.value = 360
+      isPanelVisible.value = true
+    }
+  }
+  
+  updateOlCss()
+}
+
+const handlePanelResize = (newWidth) => {
+  rightPanelWidth.value = newWidth
+}
+
+const showInfoDrawer = () => {
+  drawerVisible.value = true
+}
+
+const getMapInstance = () => {
+  return olmapRef.value?.map || null
+}
+
+const handleZoomIn = () => {
+  try {
+    const map = getMapInstance()
+    if (map) {
+      const view = map.getView()
+      view.animate({
+        zoom: view.getZoom() + 1,
+        duration: 250
+      })
+    }
+  } catch (error) {
+    console.warn('地图放大失败:', error)
+    ElMessage.warning('地图放大失败')
+  }
+}
+
+const handleZoomOut = () => {
+  try {
+    const map = getMapInstance()
+    if (map) {
+      const view = map.getView()
+      view.animate({
+        zoom: Math.max(view.getZoom() - 1, 1),
+        duration: 250
+      })
+    }
+  } catch (error) {
+    console.warn('地图缩小失败:', error)
+    ElMessage.warning('地图缩小失败')
+  }
+}
+
+const handleCenterToRobot = () => {
+  try {
+    const map = getMapInstance()
+    if (map) {
+      const view = map.getView()
+      
+      let centerPoint = [laserPositionData.x, laserPositionData.y]
+      if (laserPositionData.x === 0 && laserPositionData.y === 0) {
+        centerPoint = view.getCenter()
+      }
+      
+      view.animate({
+        center: centerPoint,
+        zoom: Math.max(view.getZoom(), 15),
+        duration: 500
+      })
+      ElMessage.success('已定位到机器人位置')
+    }
+  } catch (error) {
+    console.warn('定位机器人失败:', error)
+    ElMessage.warning('定位机器人失败')
+  }
+}
+
+const handleToggleFullscreen = () => {
+  try {
+    const mapStage = mapStageRef.value
+    if (!mapStage) return
+    
+    if (!isFullscreen.value) {
+      if (mapStage.requestFullscreen) {
+        mapStage.requestFullscreen()
+      } else if (mapStage.webkitRequestFullscreen) {
+        mapStage.webkitRequestFullscreen()
+      } else if (mapStage.mozRequestFullScreen) {
+        mapStage.mozRequestFullScreen()
+      } else if (mapStage.msRequestFullscreen) {
+        mapStage.msRequestFullscreen()
+      }
+      isFullscreen.value = true
+    } else {
+      if (document.exitFullscreen) {
+        document.exitFullscreen()
+      } else if (document.webkitExitFullscreen) {
+        document.webkitExitFullscreen()
+      } else if (document.mozCancelFullScreen) {
+        document.mozCancelFullScreen()
+      } else if (document.msExitFullscreen) {
+        document.msExitFullscreen()
+      }
+      isFullscreen.value = false
+    }
+    
+    setTimeout(() => {
+      updateOlCss()
+    }, 100)
+  } catch (error) {
+    console.warn('全屏切换失败:', error)
+    ElMessage.warning('全屏功能不可用')
+  }
+}
+
+// === Calibration Methods ===
+const addCalibration = () => {
+  if (laserPositionData.x === 0 && laserPositionData.y === 0) {
+    ElMessage.warning('当前没有有效的激光定位数据')
+    return
+  }
+  
+  const hasValidGnss = gnssPositionData.longitude !== 0 || gnssPositionData.latitude !== 0
+  if (!hasValidGnss) {
+    ElMessage.warning('当前没有有效的GNSS数据')
+    return
+  }
+  
+  // 重新编号保证ID连续
+  calibrationList.value = calibrationList.value.map((item, index) => ({
+    ...item,
+    id: index + 1
+  }))
+  
+  const newId = calibrationList.value.length + 1
+  const coordinate = `${laserPositionData.x},${laserPositionData.y}`
+  
+  const statusParts = gnssPositionData.status.split('/')
+  const rtkStatus = parseInt(statusParts[1]) || 0
+  
+  const data = { 
+    id: newId, 
+    coordinate, 
+    angle: laserPositionData.angle,
+    gnss: {
+      latitude: gnssPositionData.latitude,
+      longitude: gnssPositionData.longitude,
+      heading: gnssPositionData.angle,
+      status: rtkStatus,
+      satellites: gnssPositionData.satellites
+    }
+  }
+  
+  calibrationList.value.push(data)
+  nowCalibId.value = newId
+  
+  ElMessage.success(`已添加标定点 ${newId}`)
+  
+  nextTick(() => {
+    displayCalibrationPointsOnMap()
+  })
+}
+
+const removeCalibration = (id) => {
+  if (calibrationList.value.length < 1) return
+  
+  calibrationList.value = calibrationList.value.filter(item => item.id !== id)
+  
+  // 重新编号保证ID连续
+  calibrationList.value = calibrationList.value.map((item, index) => ({
+    ...item,
+    id: index + 1
+  }))
+  
+  nowCalibId.value = -1
+  
+  nextTick(() => {
+    displayCalibrationPointsOnMap()
+  })
+}
+
+const loadHistoryCalibrationData = async () => {
+  try {
+    const response = await getCalibrationHistory(mapName.value)
+    console.log('加载历史标定数据响应:', response.calibration)
+    
+    if (response.calibration && Array.isArray(response.calibration)) {
+      calibrationList.value = response.calibration.map((item, index) => ({
+        id: index + 1,
+        coordinate: `${item.x},${item.y}`,
+        angle: item.yaw,
+        gnss: {
+          latitude: item.b,
+          longitude: item.l,
+          heading: item.heading,
+          status: item.status,
+          satellites: item.start
+        }
+      }))
+      
+      console.log('成功加载历史标定数据:', calibrationList.value.length, '个点')
+      
+      nextTick(() => {
+        displayCalibrationPointsOnMap()
+      })
+    }
+  } catch (error) {
+    console.log('未找到历史标定数据或加载失败:', error.message)
+  }
+}
+
+const displayCalibrationPointsOnMap = () => {
+  const olmap = olmapRef.value
+  if (!olmap || calibrationList.value.length === 0) {
+    return
+  }
+  
+  const displayPoints = () => {
+    if (!olmap.map) {
+      setTimeout(displayPoints, 100)
+      return
+    }
+    
+    try {
+      olmap.calibrationList = []
+      
+      calibrationList.value.forEach(item => {
+        const coords = item.coordinate.split(',')
+        const x = parseFloat(coords[0])
+        const y = parseFloat(coords[1])
+        
+        olmap.calibrationList.push({
+          id: item.id,
+          x: x,
+          y: y
+        })
+      })
+      
+      if (olmap.refreshCalibrationOverlays) {
+        olmap.refreshCalibrationOverlays()
+      }
+      
+      console.log('已在地图上显示', calibrationList.value.length, '个历史标定点')
+    } catch (error) {
+      console.error('显示标定点失败:', error)
+    }
+  }
+  
+  displayPoints()
+}
+
+const executeCalibration = async () => {
+  if (calibrationList.value.length < 1) {
+    ElMessage.warning('请先添加标定点!')
+    return
+  }
+  
+  const hasValidGnss = gnssPositionData.longitude !== 0 || gnssPositionData.latitude !== 0
+  if (!hasValidGnss) {
+    ElMessage.warning('GNSS数据无效,请确保设备已获取定位信息')
+    return
+  }
+  
+  const calibrationData = calibrationList.value.map(item => {
+    const coords = item.coordinate.split(',')
+    const x = parseFloat(coords[0])
+    const y = parseFloat(coords[1])
+    
+    const statusParts = gnssPositionData.status.split('/')
+    const rtkStatus = parseInt(statusParts[1]) || 0
+    
+    return {
+      x: x,
+      y: y,
+      yaw: item.angle,
+      b: item.gnss ? item.gnss.latitude : gnssPositionData.latitude,
+      l: item.gnss ? item.gnss.longitude : gnssPositionData.longitude,
+      heading: item.gnss ? item.gnss.heading : gnssPositionData.angle,
+      status: item.gnss ? item.gnss.status : rtkStatus,
+      start: item.gnss ? item.gnss.satellites : gnssPositionData.satellites
+    }
+  })
+  
+  const mqttMessage = {
+    args: [
+      {
+        roadmap: mapName.value,
+        calibration: calibrationData
+      }
+    ]
+  }
+  
+  const topic = '/settings/multi_coordinates/action/calibrate'
+  
+  console.log('发送标定请求:', topic, mqttMessage)
+  
+  try {
+    await sendMqttMessage(topic, mqttMessage)
+    ElMessage.success('标定请求已发送,请等待响应...')
+  } catch (error) {
+    console.error('发送标定请求失败:', error)
+    ElMessage.error('发送标定请求失败,请稍后重试')
+  }
+}
+
+// === Watch ===
+watch(windowWidth, (newWidth) => {
+  if (newWidth < 1440) {
+    drawerVisible.value = false
+  }
+  updateRightPanelWidth()
+})
+
+watch(drawerVisible, () => {
+  nextTick(() => {
+    updateOlCss()
+  })
+})
+
+watch(isPanelVisible, (newVal) => {
+  localStorage.setItem('calibration-panel-collapsed', String(!newVal))
+  nextTick(() => {
+    updateOlCss()
+  })
+})
+
+// === Lifecycle ===
+onMounted(() => {
+  // 从 localStorage 恢复状态
+  const savedPanelWidth = localStorage.getItem('calibration-panel-width')
+  const savedPanelCollapsed = localStorage.getItem('calibration-panel-collapsed')
+  
+  if (savedPanelWidth) {
+    rightPanelWidth.value = Math.max(320, Math.min(420, parseInt(savedPanelWidth)))
+  }
+  if (savedPanelCollapsed) {
+    isPanelVisible.value = savedPanelCollapsed !== 'true'
+  }
+  
+  // 监听窗口大小变化
+  window.addEventListener('resize', handleWindowResize)
+  
+  // 监听全屏状态变化
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+  document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+  document.addEventListener('mozfullscreenchange', handleFullscreenChange)
+  document.addEventListener('MSFullscreenChange', handleFullscreenChange)
+  
+  // 初始设置面板宽度
+  updateRightPanelWidth()
+  
+  // 初始化WebSocket连接
+  setupWebSocket()
+  
+  // 加载历史标定数据
+  loadHistoryCalibrationData()
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleWindowResize)
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+  document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
+  document.removeEventListener('mozfullscreenchange', handleFullscreenChange)
+  document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
+  
+  // 断开WebSocket连接
+  disconnect()
+})
+</script>
+
+<style lang="scss" scoped>
+.calibration-container {
+  width: 100%;
+  height: calc(100vh - 84px);
+  min-height: 600px;
+  background: var(--color-bg-secondary);
+  position: relative;
+  overflow: hidden;
+
+  .map-stage {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    background: var(--color-bg-card);
+    border-radius: var(--radius-lg);
+    overflow: hidden;
+    box-shadow: var(--shadow-card);
+
+    .map-canvas-wrapper {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: 1;
+    }
+
+    .map-toolbar {
+      position: absolute;
+      left: 16px;
+      top: 16px;
+      z-index: 50;
+      pointer-events: auto;
+    }
+
+    .right-panel {
+      position: absolute;
+      right: 16px;
+      top: 16px;
+      height: calc(100% - 32px);
+      z-index: 10;
+      border-radius: var(--radius-lg);
+      box-shadow: var(--shadow-xl);
+      background: var(--color-bg-card);
+      display: flex;
+      flex-direction: column;
+      pointer-events: auto;
+      transition: all var(--duration-200) var(--ease-out);
+
+      &.panel-collapsed {
+        width: 0 !important;
+        opacity: 0;
+        pointer-events: none;
+        overflow: hidden;
+      }
+    }
+
+    .panel-reopen-btn {
+      position: absolute;
+      right: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      z-index: 12;
+      width: 36px;
+      height: 72px;
+      background: var(--color-bg-card);
+      border: 1px solid var(--color-border-primary);
+      border-radius: 8px 0 0 8px;
+      box-shadow: var(--shadow-lg);
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: var(--color-text-secondary);
+      transition: all var(--duration-200) var(--ease-out);
+      pointer-events: auto;
+
+      &:hover {
+        background: var(--color-primary);
+        color: var(--color-text-inverse);
+        border-color: var(--color-primary);
+        transform: translateY(-50%) translateX(-2px);
+        box-shadow: var(--shadow-xl);
+      }
+    }
+
+    .info-toggle-btn {
+      position: absolute;
+      top: 16px;
+      right: 16px;
+      z-index: 10;
+      pointer-events: auto;
+      
+      .el-button {
+        box-shadow: var(--shadow-lg);
+      }
+    }
+  }
+}
+
+/* 抽屉样式 */
+.drawer-content {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: var(--color-bg-card);
+
+  .drawer-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: var(--spacing-4);
+    border-bottom: 1px solid var(--color-border-secondary);
+    background: var(--color-bg-tertiary);
+
+    .drawer-title {
+      h3 {
+        margin: 0;
+        font-size: var(--font-size-lg);
+        font-weight: var(--font-weight-semibold);
+        color: var(--color-text-primary);
+        line-height: var(--line-height-tight);
+      }
+
+      .map-name {
+        font-size: var(--font-size-xs);
+        color: var(--color-danger);
+        margin-left: var(--spacing-2);
+      }
+    }
+  }
+
+  .drawer-body {
+    flex: 1;
+    overflow: hidden;
+
+    .drawer-panel {
+      height: 100%;
+      
+      :deep(.panel-header) {
+        display: none;
+      }
+      
+      :deep(.panel-content) {
+        height: 100%;
+        padding: var(--spacing-4);
+      }
+
+      :deep(.resize-handle) {
+        display: none;
+      }
+    }
+  }
+}
+
+/* 响应式设计 */
+@media (max-width: 1439px) {
+  .calibration-container {
+    .map-stage {
+      .right-panel {
+        display: none;
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .calibration-container {
+    height: calc(100vh - 60px);
+    
+    .map-stage {
+      border-radius: 0;
+
+      .map-toolbar {
+        left: 12px;
+        top: 12px;
+      }
+
+      .panel-reopen-btn {
+        width: 32px;
+        height: 60px;
+      }
+
+      .info-toggle-btn {
+        top: 12px;
+        right: 12px;
+      }
+    }
+  }
+}
+
+/* 动画效果 */
+@keyframes slideInRight {
+  from {
+    opacity: 0;
+    transform: translateX(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.calibration-container {
+  animation: slideInUp 0.3s ease-out;
+}
+
+.info-toggle-btn {
+  animation: slideInRight 0.3s ease-out 0.2s both;
+}
+
+/* 暗色主题适配 */
+html.dark {
+  .calibration-container {
+    .map-stage {
+      background: var(--color-bg-tertiary);
+      box-shadow: var(--shadow-card);
+
+      .right-panel {
+        background: var(--color-bg-tertiary);
+        border-color: var(--color-border-tertiary);
+        box-shadow: var(--shadow-xl);
+      }
+
+      .panel-reopen-btn {
+        background: var(--color-bg-tertiary);
+        border-color: var(--color-border-tertiary);
+
+        &:hover {
+          background: var(--color-primary);
+          border-color: var(--color-primary);
+        }
+      }
+    }
+  }
+
+  .drawer-content {
+    background: var(--color-bg-tertiary);
+
+    .drawer-header {
+      background: var(--color-bg-quaternary);
+      border-bottom-color: var(--color-border-tertiary);
+    }
+  }
+}
+</style>
+
+<style>
+/* 全局抽屉样式 */
+.info-drawer {
+  background-color: var(--color-bg-card);
+  box-shadow: var(--shadow-xl);
+  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
+}
+
+.info-drawer .el-drawer__body {
+  padding: 0;
+  height: 100%;
+  overflow: hidden;
+}
+
+/* 暗色主题抽屉样式 */
+html.dark .info-drawer {
+  background-color: var(--color-bg-tertiary);
+  box-shadow: var(--shadow-xl);
+}
+</style>

+ 1735 - 0
src/views/map/maplist/edit.vue

@@ -0,0 +1,1735 @@
+<template>
+  <div class="edit-page">
+    <!-- 地图容器 -->
+    <div class="map-container">
+      <!-- 左侧工具栏 -->
+      <div class="toolbar-container">
+        <MapToolbar
+          preset="edit"
+          :selected-key="currentMode"
+          @mode-change="handleModeChange"
+          @zoom-in="handleZoomIn"
+          @zoom-out="handleZoomOut"
+          @center-robot="handleCenterRobot"
+          @toggle-fullscreen="handleToggleFullscreen"
+          @save="handleSaveMap"
+        />
+      </div>
+
+      <!-- 地图组件(作为主内容区域) -->
+      <div id="map-stage" class="page-content" ref="mapContentRef">
+        <OlMap
+          ref="olmapRef"
+          :width="olWidth + 'px'"
+          :height="olHeight + 'px'"
+          backgroundColor="#F5F5F5"
+          :mapName="mapName"
+          :robotPoseData="robotPoseData"
+          :pointDraSelectEnabled="pointDraSelectEnabled"
+          :showDefaultControls="false"
+          @elementRoadInitEnd="elementRoadInitEnd"
+          @elementRoadDrawEnd="elementRoadDrawEnd"
+          @removeElementResult="removeElementResult"
+          @selectShowEleResult="selectShowEleResult"
+        />
+
+        <!-- 底部Inspector面板 -->
+        <BottomInspector
+          ref="bottomInspectorRef"
+          :visible="inspector.visible"
+          :mode="inspector.mode"
+          :data="inspector.data"
+          :loading="inspector.loading"
+          @edit="handleInspectorEdit"
+          @save="handleInspectorSave"
+          @cancel="handleInspectorCancel"
+          @close="handleInspectorClose"
+          @locate="handleInspectorLocate"
+          @height-change="handleInspectorHeightChange"
+        />
+      </div>
+
+      <!-- 右侧面板 -->
+      <div class="panel-container">
+        <RightPanel
+          mode="edit"
+          panelType="edit"
+          :overlay="true"
+          :tabs="rightPanelTabs"
+          :initial-tab="'info'"
+          :realtime-info="realtimeInfo"
+          :element-list="allElementList"
+          :selected-element="currentFeature"
+          :element-types="elementTypeCounts"
+          @add-current-point="handleAddCurrentPoint"
+          @element-select="handleElementSelect"
+          @element-edit="handleElementEdit"
+          @element-locate="handleElementLocate"
+          @element-remove="handleElementRemove"
+          @network-export="handleNetworkExport"
+          @network-import="handleNetworkImport"
+        />
+      </div>
+    </div>
+
+    <!-- 添加当前点对话框 -->
+    <el-dialog
+      v-model="addCurrentMapShow"
+      title="添加当前实时位姿点"
+      width="480px"
+      @open="openCurrentToMapDialog"
+      class="add-pose-dialog"
+      :close-on-click-modal="false"
+      center
+      destroy-on-close
+    >
+      <div class="dialog-content">
+        <el-form ref="currentPointFormRef" :model="currentRobotRecord" label-width="90px" size="small" class="pose-form">
+          <div class="form-section">
+            <h4 class="section-title">
+              <el-icon><Location /></el-icon>
+              位置信息
+            </h4>
+            <el-form-item label="X坐标(m)" prop="x">
+              <el-input-number
+                v-model="currentRobotRecord.x"
+                :precision="3"
+                :step="0.1"
+                controls-position="right"
+                placeholder="请输入X坐标值"
+                class="coordinate-input"
+                style="width: 100%;"
+              />
+            </el-form-item>
+            <el-form-item label="Y坐标(m)" prop="y">
+              <el-input-number
+                v-model="currentRobotRecord.y"
+                :precision="3"
+                :step="0.1"
+                controls-position="right"
+                placeholder="请输入Y坐标值"
+                class="coordinate-input"
+                style="width: 100%;"
+              />
+            </el-form-item>
+            <el-form-item label="Z坐标(m)" prop="z">
+              <el-input-number
+                v-model="currentRobotRecord.z"
+                :precision="3"
+                :step="0.1"
+                controls-position="right"
+                placeholder="请输入Z坐标值"
+                class="coordinate-input"
+                style="width: 100%;"
+              />
+            </el-form-item>
+          </div>
+
+          <div class="form-section">
+            <h4 class="section-title">
+              <el-icon><Guide /></el-icon>
+              方向配置
+            </h4>
+            <el-form-item label="航偏角(rad)" prop="angle">
+              <el-input-number
+                v-model="currentRobotRecord.angle"
+                :precision="3"
+                :step="0.1"
+                controls-position="right"
+                placeholder="请输入航偏角值"
+                style="width: 100%;"
+              />
+            </el-form-item>
+            <el-form-item label="航偏角使能" prop="angleEnable">
+              <el-switch v-model="currentRobotRecord.angleEnable" />
+            </el-form-item>
+          </div>
+        </el-form>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="addCurrentMapShow = false">取 消</el-button>
+          <el-button type="primary" @click="addCurrentToMap">
+            <el-icon><Check /></el-icon> 确 定
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
+import { Location, Guide, Check } from '@element-plus/icons-vue'
+import OlMap from "@/components/OlMap"
+import MapToolbar from "../components/shared/MapToolbar.vue"
+import RightPanel from "../components/shared/RightPanel.vue"
+import BottomInspector from "@/components/BottomInspector.vue"
+import { saveRoadMapGeoJson } from "@/api/robot/map"
+import { initWebSocket, disconnect } from "@/utils/websocket"
+
+defineOptions({
+  name: 'EditPage'
+})
+
+// === Route & Router ===
+const route = useRoute()
+const router = useRouter()
+
+// === Template Refs ===
+const mapContentRef = ref(null)
+const olmapRef = ref(null)
+const bottomInspectorRef = ref(null)
+const currentPointFormRef = ref(null)
+
+// === Reactive State ===
+const mapName = ref(route.params.mapName || '')
+
+const olWidth = ref(0)
+const olHeight = ref(0)
+
+const currentMode = ref('select-mode')
+const pointDraSelectEnabled = ref(true)
+
+const inspector = reactive({
+  visible: false,
+  mode: 'view',
+  data: null,
+  loading: false
+})
+
+const rightPanelTabs = ref(['info', 'elements', 'network'])
+
+const realtimeInfo = reactive({
+  currentMap: route.params.mapName || 'Unknown',
+  currentTask: '地图编辑',
+  speed: '0m/s',
+  speedCommand: '0m/s',
+  coordinates: '(0, 0, 0)',
+  heading: '0°',
+  totalDistance: '0m',
+  registrationError: '0',
+  batteryLevel: '0%'
+})
+
+const pointData = ref([])
+const lineData = ref([])
+const bowData = ref([])
+const shapeData = ref([])
+const currentFeature = ref({})
+
+const addCurrentMapShow = ref(false)
+const currentRobotRecord = reactive({
+  x: 0,
+  y: 0,
+  z: 0,
+  angle: 0,
+  angleEnable: false
+})
+
+const robotPoseData = reactive({
+  x: 0,
+  y: 0,
+  angle: 0
+})
+
+const haveDraw = ref(false)
+const wsDeviceId = ref('ld000001')
+
+// === Computed ===
+const allElementList = computed(() => {
+  return [
+    ...pointData.value,
+    ...lineData.value,
+    ...bowData.value,
+    ...shapeData.value
+  ]
+})
+
+const elementTypeCounts = computed(() => [
+  { key: 'point', label: '点', count: pointData.value.length },
+  { key: 'line', label: '线', count: lineData.value.length },
+  { key: 'curve', label: '弧', count: bowData.value.length },
+  { key: 'polygon', label: '面', count: shapeData.value.length }
+])
+
+// === Methods ===
+const updateMapSize = () => {
+  const mapContent = mapContentRef.value
+  if (mapContent) {
+    olWidth.value = mapContent.offsetWidth
+    olHeight.value = mapContent.offsetHeight
+  }
+}
+
+const updateRightWidth = (width) => {
+  const host = mapContentRef.value?.ownerDocument?.documentElement || document.documentElement
+  host.style.setProperty('--right-panel-width', `${width}px`)
+}
+
+const initEditMode = () => {
+  currentMode.value = 'select-mode'
+  pointDraSelectEnabled.value = true
+  console.log('编辑模式初始化完成,当前模式:', currentMode.value)
+}
+
+const handleBeforeUnload = (event) => {
+  if (haveDraw.value) {
+    event.preventDefault()
+    event.returnValue = ""
+  }
+}
+
+// === WebSocket ===
+const setupWebSocket = () => {
+  initWebSocket({
+    // deviceId 使用 websocket.js 中的默认值 'ld000001'
+    onConnect: () => {
+      console.log('WebSocket连接成功')
+    },
+    onDisconnect: () => {
+      console.log('WebSocket连接断开')
+    },
+    onMessage: (type, message) => {
+      handleWsMessage(type, message)
+    },
+    onError: (error) => {
+      console.error('WebSocket错误:', error)
+    }
+  })
+}
+
+const handleWsMessage = (type, message) => {
+  console.log("znm",type);
+  
+  switch (type) {
+    case 'pose':
+      handlePoseMessage(message)
+      break
+    case 'raw':
+      if (message.topic && message.topic.includes('battery')) {
+        handleBatteryMessage(message)
+      }
+      break
+  }
+}
+
+const handlePoseMessage = (data) => {
+  console.log("data:",data);
+  
+  try {
+    const pose = data.data || data
+    const { xyz, rpy } = pose
+
+    if (xyz) {
+      robotPoseData.x = xyz[0]
+      robotPoseData.y = xyz[1]
+      robotPoseData.angle = rpy ? rpy[2] : 0
+      currentRobotRecord.x = xyz[0]
+      currentRobotRecord.y = xyz[1]
+      currentRobotRecord.z = xyz[2] || 0
+      currentRobotRecord.angle = rpy ? rpy[2] : 0
+      realtimeInfo.coordinates = `(${xyz[0]},${xyz[1]},${xyz[2] || 0})`
+    }
+
+    if (rpy) {
+      realtimeInfo.heading = rpy[2] + '°'
+    }
+
+    if (pose.heading || pose.velocity) {
+      realtimeInfo.speed = (pose.heading || 0) + 'm/s'
+      realtimeInfo.speedCommand = (pose.velocity || 0) + 'm/s'
+    }
+
+    if (pose.odometer !== undefined) {
+      realtimeInfo.totalDistance = pose.odometer + 'm'
+    }
+
+    if (pose.confidence !== undefined) {
+      realtimeInfo.registrationError = pose.confidence
+    }
+  } catch (error) {
+    console.error('解析位姿消息失败:', error)
+  }
+}
+
+const handleBatteryMessage = (data) => {
+  try {
+    const payload = data.payload || data
+    if (payload.capacity !== undefined) {
+      realtimeInfo.batteryLevel = (payload.capacity * 100).toFixed(2) + '%'
+    }
+  } catch (error) {
+    console.error('解析电池消息失败:', error)
+  }
+}
+
+// === 工具栏事件处理 ===
+const handleModeChange = (mode) => {
+  const modeMap = {
+    'select-mode': 'select',
+    'draw-point': 'draw-point',
+    'draw-line': 'draw-line',
+    'draw-curve': 'draw-curve',
+    'draw-polygon': 'draw-polygon'
+  }
+
+  const internalMode = modeMap[mode] || mode
+  currentMode.value = mode
+  const olmap = olmapRef.value
+
+  switch (internalMode) {
+    case 'select':
+      pointDraSelectEnabled.value = true
+      olmap?.clearDraw?.()
+      ElMessage.success('已切换到选择模式')
+      break
+    case 'draw-point':
+      pointDraSelectEnabled.value = false
+      if (olmap?.drawPoint) {
+        olmap.drawPoint()
+        ElMessage.success('已切换到绘点模式,点击地图添加点位')
+      } else {
+        ElMessage.warning('地图组件未就绪,请稍后重试')
+      }
+      break
+    case 'draw-line':
+      pointDraSelectEnabled.value = false
+      if (olmap?.drawLine) {
+        olmap.drawLine()
+        ElMessage.success('已切换到绘线模式,点击地图绘制线段')
+      } else {
+        ElMessage.warning('地图组件未就绪,请稍后重试')
+      }
+      break
+    case 'draw-curve':
+      pointDraSelectEnabled.value = false
+      if (olmap?.drawCurve) {
+        olmap.drawCurve()
+        ElMessage.success('已切换到绘曲线模式,点击地图绘制曲线')
+      } else {
+        ElMessage.warning('地图组件未就绪,请稍后重试')
+      }
+      break
+    case 'draw-polygon':
+      pointDraSelectEnabled.value = false
+      if (olmap?.drawPolygon) {
+        olmap.drawPolygon()
+        ElMessage.success('已切换到绘区域模式,点击地图绘制多边形区域')
+      } else {
+        ElMessage.warning('地图组件未就绪,请稍后重试')
+      }
+      break
+    default:
+      console.warn('未知的绘制模式:', mode)
+      break
+  }
+}
+
+const handleZoomIn = () => {
+  const olmap = olmapRef.value
+  if (olmap?.map) {
+    const view = olmap.map.getView()
+    view.animate({ zoom: view.getZoom() + 1, duration: 250 })
+  }
+}
+
+const handleZoomOut = () => {
+  const olmap = olmapRef.value
+  if (olmap?.map) {
+    const view = olmap.map.getView()
+    view.animate({ zoom: view.getZoom() - 1, duration: 250 })
+  }
+}
+
+const handleCenterRobot = () => {
+  const olmap = olmapRef.value
+  if (olmap?.map && robotPoseData.x && robotPoseData.y) {
+    const view = olmap.map.getView()
+    view.animate({
+      center: [robotPoseData.x, robotPoseData.y],
+      duration: 300
+    })
+  }
+}
+
+const handleToggleFullscreen = () => {
+  if (!document.fullscreenElement) {
+    document.documentElement.requestFullscreen()
+  } else {
+    document.exitFullscreen()
+  }
+}
+
+const handleSaveMap = async () => {
+  if (!haveDraw.value && resourcesFeature.value.length > 0) {
+    ElMessage.info('当前没有需要保存的更改')
+    return
+  }
+
+  if (resourcesFeature.value.length === 0) {
+    ElMessage.warning('当前地图没有任何元素可保存')
+    return
+  }
+
+  try {
+    ElMessage.info('正在保存路网数据...')
+    await saveRoute()
+  } catch (error) {
+    console.error('保存失败:', error)
+  }
+}
+
+// === 右侧面板事件处理 ===
+const handleAddCurrentPoint = () => {
+  addCurrentMapShow.value = true
+}
+
+const handleElementSelect = (element) => {
+  showInspector(element, 'edit')
+  const olmap = olmapRef.value
+  if (olmap?.selectShowEle && element?.id) {
+    olmap.selectShowEle(element.id)
+  }
+}
+
+const handleElementEdit = (element) => {
+  const olmap = olmapRef.value
+  if (olmap?.selectShowEle && element?.id) {
+    olmap.selectShowEle(element.id)
+  }
+  showInspector(element, 'edit')
+}
+
+const handleElementLocate = (element) => {
+  const olmap = olmapRef.value
+  if (olmap?.selectShowEle && element?.id) {
+    olmap.selectShowEle(element.id)
+    ElMessage.success(`已定位到元素 ${element.id}`)
+  } else {
+    ElMessage.error('无法定位元素')
+  }
+}
+
+const handleElementRemove = async (element) => {
+  try {
+    await ElMessageBox.confirm(`确定要删除元素 ${element.id} 吗?`, '确认删除', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    const loading = ElLoading.service({
+      lock: true,
+      text: '正在删除元素...',
+      spinner: 'el-icon-loading',
+      background: 'rgba(0, 0, 0, 0.7)'
+    })
+
+    try {
+      const olmap = olmapRef.value
+      if (olmap?.removeElement) {
+        olmap.removeElement(element.id)
+      }
+
+      await saveRoute()
+
+      if (inspector.visible && inspector.data && inspector.data.id === element.id) {
+        inspector.visible = false
+        inspector.mode = 'view'
+        inspector.data = null
+      }
+
+      ElMessage.success(`元素 ${element.id} 删除成功`)
+    } finally {
+      loading.close()
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除元素失败:', error)
+      ElMessage.error('删除元素失败: ' + (error.message || '未知错误'))
+    }
+  }
+}
+
+// === 路网导入导出 ===
+const handleNetworkExport = async () => {
+  try {
+    if (!resourcesFeature.value || resourcesFeature.value.length === 0) {
+      ElMessage.warning('当前地图没有路网数据可导出')
+      return
+    }
+
+    const loading = ElLoading.service({
+      lock: true,
+      text: '正在准备导出数据...',
+      spinner: 'el-icon-loading',
+      background: 'rgba(0, 0, 0, 0.7)'
+    })
+
+    try {
+      const geoJsonData = buildGeoJsonData()
+
+      const exportData = {
+        ...geoJsonData,
+        metadata: {
+          exportTime: new Date().toISOString(),
+          mapName: mapName.value,
+          elementCount: geoJsonData.features.length,
+          version: '1.0'
+        }
+      }
+
+      const fileContent = JSON.stringify(exportData, null, 2)
+      const blob = new Blob([fileContent], { type: 'application/json' })
+
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
+      const fileName = `roadmap_${mapName.value}_${timestamp}.json`
+
+      const url = window.URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = fileName
+
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+
+      window.URL.revokeObjectURL(url)
+
+      ElMessage.success(`路网数据已导出:${fileName}`)
+    } finally {
+      loading.close()
+    }
+  } catch (error) {
+    console.error('路网导出失败:', error)
+    ElMessage.error('路网导出失败:' + (error.message || '未知错误'))
+  }
+}
+
+const handleNetworkImport = (type) => {
+  const fileInput = document.createElement('input')
+  fileInput.type = 'file'
+  fileInput.accept = '.json'
+  fileInput.style.display = 'none'
+
+  fileInput.addEventListener('change', async (event) => {
+    const file = event.target.files[0]
+    if (!file) return
+
+    try {
+      await processImportFile(file, type)
+    } catch (error) {
+      console.error('导入处理失败:', error)
+      ElMessage.error(`路网导入失败:` + (error.message || '未知错误'))
+    } finally {
+      document.body.removeChild(fileInput)
+    }
+  })
+
+  document.body.appendChild(fileInput)
+  fileInput.click()
+}
+
+const processImportFile = async (file, importType) => {
+  const typeMap = {
+    'import': '导入',
+    'replace': '覆盖导入',
+    'merge': '合并导入',
+    'incremental': '增量导入'
+  }
+
+  const loading = ElLoading.service({
+    lock: true,
+    text: `正在${typeMap[importType] || '导入'}路网数据...`,
+    spinner: 'el-icon-loading',
+    background: 'rgba(0, 0, 0, 0.7)'
+  })
+
+  try {
+    const fileContent = await readFileContent(file)
+
+    let importData
+    try {
+      importData = JSON.parse(fileContent)
+    } catch (parseError) {
+      throw new Error('文件格式错误:无法解析JSON数据')
+    }
+
+    validateImportData(importData)
+    await executeImportStrategy(importData, importType)
+    await refreshMapAfterImport()
+    await saveRoute()
+
+    ElMessage.success(`路网${typeMap[importType] || '导入'}成功!共处理${importData.features.length}个元素`)
+  } finally {
+    loading.close()
+  }
+}
+
+const readFileContent = (file) => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onload = e => resolve(e.target.result)
+    reader.onerror = () => reject(new Error('文件读取失败'))
+    reader.readAsText(file)
+  })
+}
+
+const validateImportData = (data) => {
+  if (!data || typeof data !== 'object') {
+    throw new Error('无效的数据格式')
+  }
+
+  if (data.type !== 'FeatureCollection') {
+    throw new Error('数据类型错误:期望FeatureCollection格式')
+  }
+
+  if (!Array.isArray(data.features)) {
+    throw new Error('数据格式错误:缺少features数组')
+  }
+
+  if (data.features.length === 0) {
+    throw new Error('导入文件中没有路网元素')
+  }
+}
+
+const executeImportStrategy = async (importData, importType) => {
+  const features = importData.features
+
+  switch (importType) {
+    case 'replace':
+      await replaceImport(features)
+      break
+    case 'merge':
+      await mergeImport(features)
+      break
+    case 'incremental':
+      await incrementalImport(features)
+      break
+    default:
+      await normalImport(features)
+      break
+  }
+}
+
+const replaceImport = async (features) => {
+  clearAllMapElements()
+  await importFeaturesToMap(features)
+}
+
+const mergeImport = async (features) => {
+  await importFeaturesToMap(features)
+}
+
+const incrementalImport = async (features) => {
+  const newFeatures = features.filter(feature => {
+    return !resourcesFeature.value.find(f => f.getId() === feature.properties.id)
+  })
+
+  if (newFeatures.length === 0) {
+    ElMessage.info('所有元素均已存在,无需导入')
+    return
+  }
+
+  await importFeaturesToMap(newFeatures)
+  ElMessage.info(`跳过${features.length - newFeatures.length}个已存在的元素`)
+}
+
+const normalImport = async (features) => {
+  await importFeaturesToMap(features)
+}
+
+const clearAllMapElements = () => {
+  const olmap = olmapRef.value
+  if (olmap?.clearAllElements) {
+    olmap.clearAllElements()
+  }
+
+  pointData.value = []
+  lineData.value = []
+  bowData.value = []
+  shapeData.value = []
+  resourcesFeature.value = []
+  currentFeature.value = {}
+}
+
+const importFeaturesToMap = async (features) => {
+  const olmap = olmapRef.value
+  if (!olmap?.importGeoJsonFeatures) {
+    throw new Error('地图组件未就绪,无法导入数据')
+  }
+
+  const geoJsonData = {
+    type: 'FeatureCollection',
+    features: features
+  }
+
+  await olmap.importGeoJsonFeatures(geoJsonData)
+}
+
+const refreshMapAfterImport = async () => {
+  await nextTick()
+
+  const olmap = olmapRef.value
+  if (olmap?.refresh) {
+    olmap.refresh()
+  }
+}
+
+// === Inspector事件处理 ===
+const showInspector = (element, mode = 'edit') => {
+  inspector.loading = true
+  inspector.visible = true
+  inspector.mode = mode
+
+  setTimeout(() => {
+    const elementData = prepareElementData(element)
+    inspector.data = elementData
+    inspector.loading = false
+    currentFeature.value = element
+  }, 100)
+}
+
+const prepareElementData = (element) => {
+  const feature = resourcesFeature.value.find(f => f.getId() === element.id)
+  if (feature) {
+    const properties = feature.getProperties()
+    const geometry = feature.getGeometry()
+
+    return {
+      ...element,
+      ...properties,
+      type: geometry.getType(),
+      position: geometry.getType() === 'Point' ? geometry.getCoordinates() : undefined
+    }
+  }
+  return element
+}
+
+const handleInspectorEdit = () => {
+  inspector.mode = 'edit'
+}
+
+const handleInspectorSave = async (elementData) => {
+  const olmap = olmapRef.value
+
+  if (!elementData || !elementData.id) {
+    ElMessage.error('保存失败:元素数据无效')
+    return
+  }
+
+  if (!resourcesFeature.value || resourcesFeature.value.length === 0) {
+    ElMessage.error('保存失败:找不到地图元素数据')
+    return
+  }
+
+  const loading = ElLoading.service({
+    lock: true,
+    text: '正在保存元素...',
+    spinner: 'el-icon-loading',
+    background: 'rgba(0, 0, 0, 0.7)'
+  })
+
+  try {
+    let targetFeature = null
+    for (let i = 0; i < resourcesFeature.value.length; i++) {
+      if (resourcesFeature.value[i].getId() === elementData.id) {
+        targetFeature = resourcesFeature.value[i]
+        break
+      }
+    }
+
+    if (!targetFeature) {
+      throw new Error(`找不到ID为 ${elementData.id} 的元素`)
+    }
+
+    const cleanData = { ...elementData }
+    delete cleanData.typeEle
+    delete cleanData.directList
+
+    targetFeature.setProperties(cleanData)
+
+    if (elementData.position && targetFeature.getGeometry().getType() === 'Point') {
+      targetFeature.getGeometry().setCoordinates(elementData.position)
+      if (olmap?.pointPositionUpdate && elementData.id) {
+        olmap.pointPositionUpdate(elementData.id, elementData.position)
+      }
+    }
+
+    if (elementData.id && elementData.id.startsWith('s')) {
+      if (olmap?.editSnapeColor) {
+        olmap.editSnapeColor(elementData.id)
+      }
+    }
+
+    currentFeature.value = { ...elementData }
+    haveDraw.value = true
+
+    updateElementInList(elementData)
+    await saveRoute()
+
+    inspector.visible = false
+
+    setTimeout(() => {
+      if (olmap?.selectShowEle && elementData.id) {
+        olmap.selectShowEle(elementData.id)
+      }
+    }, 100)
+
+    ElMessage.success(`元素 ${elementData.id} 保存成功`)
+  } catch (error) {
+    console.error('保存元素失败:', error)
+    ElMessage.error('保存元素失败: ' + (error.message || '未知错误'))
+  } finally {
+    loading.close()
+  }
+}
+
+const handleInspectorCancel = () => {
+  inspector.visible = false
+  inspector.mode = 'view'
+}
+
+const handleInspectorClose = () => {
+  inspector.visible = false
+  inspector.mode = 'view'
+  inspector.data = null
+}
+
+const handleInspectorLocate = (element) => {
+  const olmap = olmapRef.value
+  if (olmap?.selectShowEle && element?.id) {
+    olmap.selectShowEle(element.id)
+  }
+}
+
+const handleInspectorHeightChange = (height) => {
+  console.log('Inspector高度变化为:', height)
+}
+
+const updateElementInList = (elementData) => {
+  const updateElementInArray = (array) => {
+    const index = array.findIndex(item => item.id === elementData.id)
+    if (index !== -1) {
+      array[index] = { ...array[index], ...elementData }
+    }
+  }
+
+  if (elementData.id.startsWith('p')) {
+    updateElementInArray(pointData.value)
+  } else if (elementData.id.startsWith('l')) {
+    updateElementInArray(lineData.value)
+  } else if (elementData.id.startsWith('b')) {
+    updateElementInArray(bowData.value)
+  } else if (elementData.id.startsWith('s')) {
+    updateElementInArray(shapeData.value)
+  }
+}
+
+// === 地图事件处理 ===
+const resourcesFeature = ref([])
+
+const elementRoadInitEnd = (features) => {
+  features.forEach(item => {
+    if (item.values_ && item.values_.id) {
+      const id = item.values_.id
+      const data = {
+        id: id,
+        name: item.values_.name,
+        type: item.getGeometry().getType()
+      }
+      if (id.startsWith('p')) {
+        pointData.value.push(data)
+      } else if (id.startsWith('l')) {
+        lineData.value.push(data)
+      } else if (id.startsWith('b')) {
+        bowData.value.push(data)
+      } else if (id.startsWith('s')) {
+        shapeData.value.push(data)
+      }
+    }
+  })
+
+  pointData.value.sort((a, b) => parseInt(a.id.split('_')[1]) - parseInt(b.id.split('_')[1]))
+  lineData.value.sort((a, b) => parseInt(a.id.split('_')[1]) - parseInt(b.id.split('_')[1]))
+  bowData.value.sort((a, b) => parseInt(a.id.split('_')[1]) - parseInt(b.id.split('_')[1]))
+  shapeData.value.sort((a, b) => parseInt(a.id.split('_')[1]) - parseInt(b.id.split('_')[1]))
+
+  resourcesFeature.value = features.filter(feature => feature !== null && feature !== undefined && typeof feature.getGeometry === 'function')
+  console.log(`elementRoadInitEnd: 原始feature数量=${features.length}, 有效feature数量=${resourcesFeature.value.length}`)
+}
+
+const elementRoadDrawEnd = (feature) => {
+  const id = feature.values_.id || feature.getId()
+  const data = {
+    id: id,
+    name: feature.values_.name,
+    type: feature.getGeometry().getType()
+  }
+  if (id.startsWith('p')) {
+    pointData.value.push(data)
+    initElelmentParamsPonint(feature)
+  } else if (id.startsWith('l')) {
+    lineData.value.push(data)
+    initElelmentParamsBowOrLine(feature)
+  } else if (id.startsWith('b')) {
+    bowData.value.push(data)
+    initElelmentParamsBowOrLine(feature)
+  } else if (id.startsWith('s')) {
+    shapeData.value.push(data)
+    initElelmentParamsSnape(feature)
+  }
+  haveDraw.value = true
+}
+
+const selectShowEleResult = (feature) => {
+  const geometry = feature.getGeometry()
+
+  if (geometry.getType() == 'Point') {
+    feature.set('position', geometry.getCoordinates())
+  } else if (geometry.getType() == "LineString") {
+    const value = feature.values_.direct - 100 || 0
+    const binary = value.toString(2).padStart(4, '0')
+    const result = [
+      binary[0] === '1',
+      binary[1] === '1',
+      binary[2] === '1',
+      binary[3] === '1'
+    ]
+    feature.set('directList', result)
+  }
+  feature.set('typeEle', geometry.getType())
+
+  const elementData = {
+    id: feature.getId(),
+    name: feature.get('name') || '',
+    type: geometry.getType(),
+    ...feature.getProperties()
+  }
+
+  showInspector(elementData, 'edit')
+}
+
+const removeElementResult = (id) => {
+  if (!id) {
+    ElMessage.error('移除元素失败!')
+    return
+  }
+
+  if (id.startsWith('p')) {
+    const index = pointData.value.findIndex(item => item.id === id)
+    if (index !== -1) {
+      pointData.value.splice(index, 1)
+    }
+  }
+  if (id.startsWith('l')) {
+    const index = lineData.value.findIndex(item => item.id === id)
+    if (index !== -1) {
+      lineData.value.splice(index, 1)
+    }
+  }
+  if (id.startsWith('b')) {
+    const index = bowData.value.findIndex(item => item.id === id)
+    if (index !== -1) {
+      bowData.value.splice(index, 1)
+    }
+  }
+  if (id.startsWith('s')) {
+    const index = shapeData.value.findIndex(item => item.id === id)
+    if (index !== -1) {
+      shapeData.value.splice(index, 1)
+    }
+  }
+
+  if (currentFeature.value && currentFeature.value.id == id) {
+    currentFeature.value = {}
+  }
+
+  const index = resourcesFeature.value.findIndex(feature => feature && feature.getId && feature.getId() == id)
+  if (index !== -1) {
+    resourcesFeature.value.splice(index, 1)
+  }
+
+  resourcesFeature.value = resourcesFeature.value.filter(feature => feature !== null && feature !== undefined && typeof feature.getGeometry === 'function')
+
+  haveDraw.value = true
+}
+
+const initElelmentParamsPonint = (feature) => {
+  feature.set('name', '')
+  feature.set('yaw', 0)
+  feature.set('isyawfix', feature.get('isyawfix') ? true : false)
+  feature.set('taskid', 0)
+  feature.set('offset', 0)
+  feature.set('pitch', 0)
+  feature.set('roll', 0)
+
+  if (feature && typeof feature.getGeometry === 'function') {
+    resourcesFeature.value.push(feature)
+  }
+}
+
+const initElelmentParamsBowOrLine = (feature) => {
+  feature.set('name', '')
+  feature.set('direct', 110)
+  feature.set('chassistype', 10)
+  feature.set('plantype', 0)
+  feature.set('controltype', 0)
+  feature.set('obstype', 0)
+  feature.set('runtype', 0)
+  feature.set('roadwidth', 0)
+  feature.set('lanewidth', 1.0)
+  feature.set('leftlanenum', 0)
+  feature.set('rightlanenum', 0)
+  feature.set('maxspeed', 0.5)
+  feature.set('minspeed', 0.0)
+  feature.set('maxtheta', 1.0)
+  feature.set('mintheta', -1.0)
+  feature.set('selftheta', 0.5)
+  feature.set('xytolerance', 0.2)
+  feature.set('thtolerance', 0.2)
+  feature.set('slowdis', 2.0)
+  feature.set('followdis', 0.4)
+  feature.set('obsvalue', 200)
+  feature.set('s2eforward', 0.0)
+  feature.set('e2sforward', 0.0)
+
+  if (feature && typeof feature.getGeometry === 'function') {
+    resourcesFeature.value.push(feature)
+  }
+}
+
+const initElelmentParamsSnape = (feature) => {
+  feature.set('name', '')
+  feature.set('typeEle', feature.getGeometry().getType())
+  feature.set('color', "#7EFFFA")
+  feature.set('type', 1)
+  feature.set('transparent', 200)
+
+  if (feature && typeof feature.getGeometry === 'function') {
+    resourcesFeature.value.push(feature)
+  }
+}
+
+// === 保存路网 ===
+const saveRoute = async () => {
+  try {
+    const geoJsonData = buildGeoJsonData()
+
+    await saveRoadMapToServer(geoJsonData)
+
+    haveDraw.value = false
+
+    if (geoJsonData.features.length === 0) {
+      ElMessage.success('路网已清空并保存')
+    } else {
+      ElMessage.success(`路网数据保存成功,共${geoJsonData.features.length}个元素`)
+    }
+  } catch (error) {
+    console.error('保存路网数据失败:', error)
+    ElMessage.error('保存路网数据失败: ' + (error.message || '未知错误'))
+    throw error
+  }
+}
+
+const cleanCoordinates = (coordinates) => {
+  if (!coordinates) return coordinates
+
+  if (Array.isArray(coordinates)) {
+    return coordinates.map(coord => {
+      if (Array.isArray(coord)) {
+        return cleanCoordinates(coord)
+      } else {
+        return (coord === null || coord === undefined) ? 0 : coord
+      }
+    })
+  }
+
+  return coordinates
+}
+
+const buildGeoJsonData = () => {
+  // 验证resourcesFeature数组
+  if (!resourcesFeature.value || resourcesFeature.value.length === 0) {
+    console.warn('buildGeoJsonData: resourcesFeature数组为空')
+    return {
+      type: "FeatureCollection",
+      features: [],
+      map: mapName.value
+    }
+  }
+
+  console.log('构建GeoJSON数据,元素数量:', resourcesFeature.value.length)
+
+  // 首先过滤掉null或undefined的feature对象
+  const validFeatures = resourcesFeature.value.filter(feature => feature !== null && feature !== undefined)
+  console.log('有效元素数量:', validFeatures.length)
+
+  const features = validFeatures.map(feature => {
+    try {
+      // 再次确认feature是有效的
+      if (!feature || typeof feature.getProperties !== 'function') {
+        console.warn('发现无效的feature对象:', feature)
+        return null
+      }
+
+      const properties = { ...feature.getProperties() }
+      const geometry = feature.getGeometry()
+
+      // 验证几何对象
+      if (!geometry) {
+        console.warn('发现无效的几何对象,跳过元素:', feature.getId())
+        return null
+      }
+
+      // 确保包含ID
+      if (!properties.id) {
+        properties.id = feature.getId()
+      }
+
+      // 清理临时渲染属性(不修改原始数据,只在保存时清理)
+      const cleanProperties = { ...properties }
+      // 先保存贝塞尔控制点,因为后续处理需要用到
+      const bezierControlPoints = cleanProperties.bezierControlPoints
+      delete cleanProperties.typeEle      // 删除临时类型属性
+      delete cleanProperties.position     // 删除临时位置属性
+      delete cleanProperties.directList   // 删除临时方向列表属性
+      delete cleanProperties.geometry     // 删除OpenLayers几何对象,避免嵌套
+      // 注意:不在此处删除bezierControlPoints,让它保留在cleanProperties中用于后续处理
+
+      // 删除可能存在的错误type字段(LineString/Point等)
+      // 这个字段可能来自历史数据,会干扰导航算法
+      // 注意:Polygon的type字段是合法的区域类型(0-22),不应删除
+      if (geometry.getType() !== 'Polygon' && cleanProperties.type !== undefined) {
+        console.warn(`删除元素 ${properties.id} 中的错误type字段:`, cleanProperties.type)
+        delete cleanProperties.type
+      }
+
+      // 获取原始坐标并清理null值
+      const originalCoordinates = geometry.getCoordinates()
+      const cleanedCoordinates = cleanCoordinates(originalCoordinates)
+
+      // 根据几何类型处理线段导出
+      let exportGeometryType = geometry.getType()
+      let exportCoordinates = cleanedCoordinates
+
+      // 处理LineString类型的元素
+      if (geometry.getType() === 'LineString') {
+        const featureId = properties.id
+
+        if (featureId && featureId.startsWith('b_')) {
+          // 贝塞尔曲线:导出为MultiPoint,使用保存的完整控制点
+          exportGeometryType = 'MultiPoint'
+
+          // 优先使用保存的原始控制点(bezierControlPoints属性)
+          if (bezierControlPoints && Array.isArray(bezierControlPoints)) {
+            exportCoordinates = cleanCoordinates(bezierControlPoints)
+            console.log(`贝塞尔曲线 ${featureId} 转换: LineString(${cleanedCoordinates.length}点) -> MultiPoint(${exportCoordinates.length}控制点)`)
+          } else {
+            // 兼容旧数据:如果没有保存控制点,只使用起点和终点
+            exportCoordinates = [
+              cleanedCoordinates[0],  // 起点
+              cleanedCoordinates[cleanedCoordinates.length - 1]  // 终点
+            ]
+            console.warn(`贝塞尔曲线 ${featureId} 缺少控制点数据,只保存起点和终点`)
+          }
+        } else {
+          // 普通LineString:只保存起点和终点
+          // 避免中间可能被错误插入的顶点影响导航
+          exportGeometryType = 'LineString'
+          exportCoordinates = [
+            cleanedCoordinates[0],  // 起点
+            cleanedCoordinates[cleanedCoordinates.length - 1]  // 终点
+          ]
+          console.log(`普通线段 ${featureId} 优化: LineString(${cleanedCoordinates.length}点) -> LineString(2点)`)
+        }
+      }
+
+      // 修复Point坐标:确保Point类型有3个坐标值(x, y, z)
+      if (exportGeometryType === 'Point' && exportCoordinates.length === 2) {
+        exportCoordinates.push(0.0) // 添加z坐标
+      }
+
+      return {
+        type: "Feature",
+        properties: cleanProperties,
+        geometry: {
+          type: exportGeometryType,
+          coordinates: exportCoordinates
+        }
+      }
+    } catch (error) {
+      console.error('处理元素时出错:', feature.getId(), error)
+      return null
+    }
+  }).filter(feature => feature !== null) // 过滤掉无效的元素
+
+  const result = {
+    type: "FeatureCollection",
+    features: features,
+    map: mapName.value
+  }
+
+  console.log('构建完成,有效元素数量:', features.length)
+  return result
+}
+
+const saveRoadMapToServer = async (geoJsonData) => {
+  try {
+    const response = await saveRoadMapGeoJson(geoJsonData)
+
+    if (response && response.status === true) {
+      return response
+    } else {
+      throw new Error('保存失败:服务器返回状态为false')
+    }
+  } catch (error) {
+    console.error('保存路网数据失败:', error)
+    throw error
+  }
+}
+
+// === 添加当前点对话框处理 ===
+const openCurrentToMapDialog = () => {
+  currentRobotRecord.x = robotPoseData.x || 0
+  currentRobotRecord.y = robotPoseData.y || 0
+  currentRobotRecord.z = 0
+  currentRobotRecord.angle = robotPoseData.angle || 0
+  currentRobotRecord.angleEnable = false
+}
+
+const addCurrentToMap = async () => {
+  const formEl = currentPointFormRef.value
+  if (!formEl) return
+
+  try {
+    await formEl.validate()
+    addCurrentMapShow.value = false
+    const data = [
+      { yaw: currentRobotRecord.angle },
+      { isyawfix: currentRobotRecord.angleEnable }
+    ]
+    const olmap = olmapRef.value
+    olmap?.createPointAtCoordinateForImport?.(
+      [currentRobotRecord.x, currentRobotRecord.y, currentRobotRecord.z || 0],
+      '',
+      data
+    )
+    ElMessage.success('已添加当前位姿点到地图')
+  } catch {
+    // 验证失败,不做任何操作
+  }
+}
+
+// === Route Leave Guard ===
+import { onBeforeRouteLeave } from 'vue-router'
+
+onBeforeRouteLeave((to, from) => {
+  const inspectorRef = bottomInspectorRef.value
+  const inspectorHasChanges = inspector.visible && inspector.mode === 'edit' && inspectorRef?.hasUnsavedChanges
+
+  if (haveDraw.value || inspectorHasChanges) {
+    const message = inspectorHasChanges
+      ? 'Inspector中有未保存的元素更改,确定要跳转或退出?'
+      : '当前页面的地图修改还未保存,确定要跳转或退出?'
+
+    return ElMessageBox.confirm(message, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }).then(() => {
+      inspector.visible = false
+      return true
+    }).catch(() => {
+      return false
+    })
+  }
+  return true
+})
+
+// === Lifecycle ===
+onMounted(() => {
+  updateMapSize()
+  initEditMode()
+  updateRightWidth(380)
+  window.addEventListener('resize', updateMapSize)
+  window.addEventListener("beforeunload", handleBeforeUnload)
+
+  // 初始化WebSocket连接
+    setupWebSocket()
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener("beforeunload", handleBeforeUnload)
+  window.removeEventListener('resize', updateMapSize)
+  // 断开WebSocket连接
+  disconnect()
+})
+</script>
+
+<style lang="scss" scoped>
+@import '../components/shared/_map-shared.css';
+
+.edit-page {
+  height: calc(100vh - 84px);
+  display: flex;
+  flex-direction: column;
+  background: #f5f7fa;
+  position: relative;
+  overflow: hidden;
+
+  --left-safe: 88px;
+  --right-panel-width: 380px;
+  --right-gutter: 32px;
+  --right-safe: calc(var(--right-panel-width) + var(--right-gutter));
+}
+
+.map-container {
+  flex: 1;
+  display: flex;
+  position: relative;
+  min-height: 0;
+}
+
+.toolbar-container {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  z-index: 11;
+  pointer-events: none;
+
+  > * {
+    pointer-events: auto;
+  }
+}
+
+#map-stage {
+  flex: 1;
+  position: relative;
+  background: #f5f5f5;
+  overflow: hidden;
+}
+
+.panel-container {
+  // 编辑页面板现在使用 fixed 定位,容器不需要特殊样式
+  pointer-events: none;
+  
+  > * {
+    pointer-events: auto;
+  }
+}
+
+// 对话框样式优化
+:deep(.el-dialog) {
+  border-radius: 12px;
+  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+
+  .el-dialog__header {
+    padding: 20px 24px 10px;
+    border-bottom: 1px solid #e4e7ed;
+
+    .el-dialog__title {
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .el-dialog__body {
+    padding: 20px 24px;
+  }
+
+  .el-dialog__footer {
+    padding: 10px 24px 20px;
+    text-align: right;
+
+    .el-button {
+      margin-left: 12px;
+
+      &:first-child {
+        margin-left: 0;
+      }
+    }
+  }
+}
+
+// 表单样式优化
+:deep(.el-form) {
+  .el-form-item {
+    margin-bottom: 18px;
+
+    .el-form-item__label {
+      font-weight: 500;
+      color: #374151;
+      line-height: 1.6;
+    }
+
+    .el-form-item__content {
+      line-height: 1.6;
+    }
+  }
+
+  .el-input {
+    .el-input__inner {
+      border-radius: 8px;
+      border: 1px solid #d1d5db;
+      transition: all 0.2s ease;
+
+      &:focus {
+        border-color: #3b82f6;
+        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+      }
+    }
+  }
+
+  .el-input-number {
+    width: 100%;
+
+    .el-input__inner {
+      border-radius: 8px;
+      border: 1px solid #d1d5db;
+      text-align: left;
+
+      &:focus {
+        border-color: #3b82f6;
+        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+      }
+    }
+  }
+
+  .el-switch {
+    .el-switch__core {
+      border-radius: 12px;
+    }
+  }
+}
+
+.centered-dialog {
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+}
+
+.add-pose-dialog {
+  :deep(.el-dialog) {
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+    margin: 0 auto;
+    position: relative;
+    top: 50%;
+    transform: translateY(-50%);
+    max-height: 90vh;
+    overflow-y: auto;
+  }
+
+  :deep(.el-dialog__wrapper) {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-height: 100vh;
+  }
+
+  :deep(.el-dialog) {
+    margin-top: 0 !important;
+    margin-bottom: 0 !important;
+  }
+
+  :deep(.el-dialog__header) {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    padding: 20px 24px;
+    margin: 0;
+    border-radius: 12px 12px 0 0;
+
+    .el-dialog__title {
+      color: white;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .el-dialog__close {
+      color: white;
+      font-size: 18px;
+
+      &:hover {
+        color: #f0f0f0;
+      }
+    }
+  }
+
+  :deep(.el-dialog__body) {
+    padding: 24px;
+    background: #f8fafc;
+    margin: 0;
+    max-height: calc(90vh - 120px);
+    overflow-y: auto;
+  }
+
+  :deep(.el-dialog__footer) {
+    padding: 16px 24px 24px;
+    background: #f8fafc;
+    border-top: 1px solid #e2e8f0;
+    border-radius: 0 0 12px 12px;
+    margin: 0;
+  }
+}
+
+.pose-form {
+  .el-form-item {
+    margin-bottom: 14px;
+    display: flex;
+    align-items: center;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .el-form-item__content {
+      flex: 1;
+      margin-left: 0;
+    }
+  }
+
+  .coordinate-input {
+    margin-bottom: 4px;
+  }
+
+  .el-form-item__label {
+    font-weight: 600;
+    color: #2d3748;
+    padding-right: 12px;
+    min-width: 100px;
+    font-size: 14px;
+    line-height: 44px;
+    height: 44px;
+    display: flex;
+    align-items: center;
+    margin-bottom: 0;
+  }
+
+  .coordinate-input {
+    :deep(.el-input__inner) {
+      border-radius: 8px;
+      border: 1px solid #e2e8f0;
+      height: 44px;
+      font-size: 15px;
+      padding: 0 16px;
+      background: #ffffff;
+      transition: all 0.2s ease;
+
+      &:focus {
+        border-color: #667eea;
+        box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+        background: #fafbff;
+      }
+
+      &:hover {
+        border-color: #cbd5e0;
+      }
+    }
+  }
+
+  :deep(.el-input-number) {
+    width: 100%;
+
+    .el-input__inner {
+      border-radius: 6px;
+      border: 1px solid #e2e8f0;
+      height: 40px;
+      font-size: 14px;
+      padding: 0 12px;
+
+      &:focus {
+        border-color: #667eea;
+        box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+      }
+    }
+  }
+}
+
+.dialog-content {
+  .form-section {
+    background: white;
+    border-radius: 10px;
+    padding: 20px;
+    margin-bottom: 16px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    border: 1px solid #f1f5f9;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      margin: 0 0 16px 0;
+      font-size: 15px;
+      font-weight: 600;
+      color: #2d3748;
+      border-bottom: 2px solid #e2e8f0;
+      padding-bottom: 8px;
+
+      .el-icon {
+        margin-right: 10px;
+        color: #667eea;
+        font-size: 18px;
+      }
+    }
+  }
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+// 响应式适配
+@media (max-width: 1200px) {
+  .panel-container {
+    right: 8px;
+    top: 8px;
+  }
+  
+  .toolbar-container {
+    left: 8px;
+    top: 8px;
+  }
+}
+
+@media (max-width: 768px) {
+  .edit-page {
+    .map-container {
+	flex-direction: column;
+    }
+    
+    .panel-container {
+      position: relative;
+      top: auto;
+      right: auto;
+	width: 100%;
+      z-index: auto;
+    }
+    
+    .toolbar-container {
+	position: relative;
+      top: auto;
+      left: auto;
+      z-index: auto;
+	margin-bottom: 8px;
+    }
+  }
+}
+
+.edit-page {
+  animation: fadeInUp 0.3s ease-out;
+}
+</style>

+ 1240 - 0
src/views/map/maplist/index.vue

@@ -0,0 +1,1240 @@
+<template>
+  <div class="map-list-container">
+    <!-- 顶部工具栏 -->
+    <div class="toolbar-section">
+      <!-- 搜索和筛选区 -->
+      <div class="toolbar-filters">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索地图名称..."
+          :prefix-icon="Search"
+          clearable
+          class="search-input"
+          @input="handleSearch"
+        />
+
+        <el-select
+          v-model="statusFilter"
+          placeholder="状态筛选"
+          clearable
+          class="status-filter"
+          @change="handleFilterChange"
+        >
+          <el-option label="全部状态" value="all" />
+          <el-option label="正常" value="available" />
+          <el-option label="不可用" value="unavailable" />
+          <el-option label="正在建图" value="building" />
+          <el-option label="正在录制" value="recording" />
+          <el-option label="实时建图" value="slaming" />
+        </el-select>
+
+        <el-select
+          v-model="sortField"
+          placeholder="排序方式"
+          class="sort-select"
+          @change="handleSortChange"
+        >
+          <el-option label="最近修改" value="updated" />
+          <el-option label="名称" value="name" />
+          <el-option label="状态" value="status" />
+        </el-select>
+      </div>
+
+      <!-- 操作按钮区 -->
+      <div class="toolbar-actions">
+        <el-button
+          type="default"
+          :icon="Upload"
+          @click="handleImport"
+        >
+          导入地图
+        </el-button>
+
+        <el-button
+          type="primary"
+          :icon="Plus"
+          @click="handleCreate"
+        >
+          新建地图
+        </el-button>
+
+        <el-button
+          type="success"
+          :icon="MapLocation"
+          @click="handleSlam"
+        >
+          实时建图
+        </el-button>
+
+        <el-button-group class="view-toggle">
+          <el-button
+            :type="viewMode === 'card' ? 'primary' : 'default'"
+            :icon="Grid"
+            @click="setViewMode('card')"
+            title="卡片视图"
+          />
+          <el-button
+            :type="viewMode === 'table' ? 'primary' : 'default'"
+            :icon="Menu"
+            @click="setViewMode('table')"
+            title="表格视图"
+          />
+        </el-button-group>
+      </div>
+    </div>
+
+    <!-- 主要内容区域 -->
+    <div class="main-content">
+      <!-- 卡片视图 -->
+      <template v-if="viewMode === 'card'">
+        <div v-loading="loading" class="card-grid" :class="{ compact: isCompactMode }">
+          <div v-if="mapList.length === 0 && !loading" class="empty-state">
+            <el-icon class="empty-icon"><MapLocation/></el-icon>
+            <h3>暂无地图</h3>
+            <p>{{ hasSearchOrFilter ? '没有找到符合条件的地图' : '您还没有创建任何地图' }}</p>
+            <el-button v-if="!hasSearchOrFilter" type="primary" @click="handleCreate">新建地图</el-button>
+          </div>
+
+          <el-card
+            v-for="item in displayedList"
+            :key="item.id"
+            class="map-card"
+            shadow="hover"
+            @click.stop="handleCardClick(item)"
+          >
+            <div class="card-thumbnail">
+              <img v-if="item.thumbUrl" :src="item.thumbUrl" alt="缩略图" />
+              <div v-else class="thumbnail-placeholder">
+                <el-icon :size="40"><MapLocation/></el-icon>
+              </div>
+            </div>
+            <div class="card-info">
+              <h4 class="map-name">{{ item.map || item.mapName || item.name }}</h4>
+              <el-tag :type="getStatusType(item.state)" size="small">{{ getStatusText(item.state) }}</el-tag>
+            </div>
+            <div class="card-actions" @click.stop>
+              <el-button link type="primary" @click.stop="handleNavigation(item)">导航</el-button>
+              <el-button link type="primary" @click.stop="handleEdit(item)">编辑</el-button>
+              <el-dropdown trigger="click" @command="cmd => handleCommand(item, cmd)">
+                <el-button link type="primary">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item command="rename">重命名</el-dropdown-item>
+                    <el-dropdown-item command="download">下载地图</el-dropdown-item>
+                    <el-dropdown-item command="build">构建地图</el-dropdown-item>
+                    <el-dropdown-item command="calibrate">坐标系标定</el-dropdown-item>
+                    <el-dropdown-item command="delete" divided>删除</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </el-card>
+        </div>
+      </template>
+
+      <!-- 表格视图 -->
+      <template v-else>
+        <el-table
+          v-loading="loading"
+          :data="displayedList"
+          row-key="id"
+          class="map-table"
+          stripe
+        >
+          <el-table-column type="index" label="序号" width="80" align="center" />
+          <el-table-column prop="map" label="地图名称" min-width="200" show-overflow-tooltip />
+          <el-table-column label="状态" width="120" align="center">
+            <template #default="{ row }">
+              <el-tag :type="getStatusType(row.state)" size="small">{{ getStatusText(row.state) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="200" align="center">
+            <template #default="{ row }">
+              <el-button link type="primary" @click="handleNavigation(row)">导航</el-button>
+              <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+              <el-button link type="primary" @click="handleCommand(row, 'delete')">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </div>
+
+    <!-- 分页 -->
+    <div v-if="totalCount > pageSize" class="pagination-section">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :total="totalCount"
+        :page-sizes="[12, 24, 48, 96]"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handlePageChange"
+      />
+    </div>
+
+    <!-- 新建地图对话框(事后建图模式:先录制再构建) -->
+    <el-dialog v-model="createDialogVisible" title="新建地图" width="500px" append-to-body>
+      <el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
+        <el-form-item label="地图名称" prop="mapName">
+          <el-input
+            v-model="createForm.mapName"
+            placeholder="请输入地图名称"
+            
+          />
+        </el-form-item>
+        <el-form-item label="建图模式" prop="mode">
+          <el-radio-group v-model="createMode" size="small">
+            <el-radio label="record">录制模式(事后建图)</el-radio>
+            <el-radio label="slam">实时建图模式</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <el-alert
+        v-if="createMode === 'record'"
+        type="info"
+        :closable="false"
+        show-icon
+        style="margin-top: 8px;"
+      >
+        <template #title>
+          录制模式说明:
+          <ul style="margin: 4px 0 0 16px; padding: 0;">
+            <li>点击"开始录制"后,系统将启动传感器数据录制</li>
+            <li>请控制机器人在场地内移动,采集环境数据</li>
+            <li>录制完成后点击"停止录制"</li>
+            <li>然后进行"构建地图"操作生成最终地图</li>
+          </ul>
+        </template>
+      </el-alert>
+      <template #footer>
+        <el-button @click="createDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitCreate">
+          {{ createMode === 'record' ? '开始录制' : '开始实时建图' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 录制进度对话框 -->
+    <el-dialog
+      v-model="recordingDialogVisible"
+      title="正在录制地图"
+      width="520px"
+      append-to-body
+      :close-on-click-modal="false"
+      :show-close="false"
+    >
+      <div class="dialog-content">
+        <div class="info-row">
+          <span class="label">地图名称:</span>
+          <span class="value"><strong>{{ recordingMapName }}</strong></span>
+        </div>
+        <div class="info-row">
+          <span class="label">录制大小:</span>
+          <span class="value"><strong>{{ formatFileSize(recordingProgress) }}</strong></span>
+        </div>
+        <div class="info-row">
+          <span class="label">当前状态:</span>
+          <el-tag type="warning" size="small">正在录制中</el-tag>
+        </div>
+      </div>
+      <el-alert
+        type="info"
+        :closable="false"
+        show-icon
+        style="margin-top: 16px;"
+      >
+        <template #title>
+          录制提示:
+          <ul style="margin: 4px 0 0 16px; padding: 0;">
+            <li>请控制机器人移动,采集环境数据</li>
+            <li>录制过程中请保持设备连接稳定</li>
+            <li>完成后请点击"停止录制"按钮</li>
+          </ul>
+        </template>
+      </el-alert>
+      <template #footer>
+        <el-button @click="recordingDialogVisible = false" :disabled="true" size="small">关闭</el-button>
+        <el-button type="warning" @click="handleStopRecord" icon="VideoPause">停止录制</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 构建地图进度对话框 -->
+    <el-dialog
+      v-model="buildingDialogVisible"
+      title="正在构建地图"
+      width="520px"
+      append-to-body
+      :close-on-click-modal="false"
+      :show-close="false"
+    >
+      <div class="dialog-content">
+        <div class="info-row">
+          <span class="label">地图名称:</span>
+          <span class="value"><strong>{{ buildingMapName }}</strong></span>
+        </div>
+        <div class="info-row">
+          <span class="label">构建进度:</span>
+          <span class="value"><strong>{{ buildingProgress }}%</strong></span>
+        </div>
+        <div class="progress-wrapper">
+          <el-progress :percentage="buildingProgress" :stroke-width="16" status="warning" />
+        </div>
+      </div>
+      <el-alert
+        type="info"
+        :closable="false"
+        show-icon
+        style="margin-top: 16px;"
+      >
+        <template #title>
+          构建提示:
+          <ul style="margin: 4px 0 0 16px; padding: 0;">
+            <li>正在处理传感器数据并生成地图文件</li>
+            <li>构建时间取决于数据量大小</li>
+            <li>请耐心等待,不要关闭此窗口</li>
+          </ul>
+        </template>
+      </el-alert>
+      <template #footer>
+        <el-button @click="buildingDialogVisible = false" :disabled="true" size="small">关闭</el-button>
+        <el-button type="danger" @click="handleStopBuild" icon="Close">取消构建</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 实时建图对话框 -->
+    <el-dialog
+      v-model="slamDialogVisible"
+      title="实时建图进行中"
+      width="520px"
+      append-to-body
+      :close-on-click-modal="false"
+      :show-close="false"
+    >
+      <div class="dialog-content">
+        <div class="info-row">
+          <span class="label">地图名称:</span>
+          <span class="value"><strong>{{ slamMapName }}</strong></span>
+        </div>
+        <div class="info-row">
+          <span class="label">当前状态:</span>
+          <el-tag type="success" size="small">正在实时建图</el-tag>
+        </div>
+      </div>
+      <el-alert
+        type="success"
+        :closable="false"
+        show-icon
+        style="margin-top: 16px;"
+      >
+        <template #title>
+          实时建图提示:
+          <ul style="margin: 4px 0 0 16px; padding: 0;">
+            <li>请控制机器人在场地内移动</li>
+            <li>系统正在实时构建三维地图</li>
+            <li>完成后点击"停止建图"自动生成地图文件</li>
+            <li>停止后需要较长时间生成最终地图</li>
+          </ul>
+        </template>
+      </el-alert>
+      <template #footer>
+        <el-button type="success" @click="handleStopSlam" icon="Check">停止建图</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 下载对话框 -->
+    <el-dialog v-model="downloadDialogVisible" title="下载地图" width="500px" append-to-body>
+      <p style="margin-bottom: 16px;">请选择要下载的地图组件:</p>
+      <el-checkbox-group v-model="downLoadTypes">
+        <el-checkbox v-for="comp in availableComponents" :key="comp.label" :label="comp.label">{{ comp.name || comp.label }}</el-checkbox>
+      </el-checkbox-group>
+      <template #footer>
+        <el-button @click="downloadDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitDownload">开始下载</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 重命名对话框 -->
+    <el-dialog v-model="renameDialogVisible" title="重命名地图" width="400px" append-to-body>
+      <el-form label-width="100px">
+        <el-form-item label="新名称">
+          <el-input v-model="newMapName" placeholder="请输入新名称" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="renameDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitRename">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 构建地图配置对话框 -->
+    <el-dialog v-model="buildConfigDialogVisible" title="构建地图" width="500px" append-to-body>
+      <el-form label-width="120px">
+        <el-form-item label="地图名称">
+          <span>{{ buildConfigMapName }}</span>
+        </el-form-item>
+        <el-form-item label="构建步骤">
+          <el-checkbox-group v-model="selectedBuildSteps">
+            <el-checkbox label="recon">重建(Recon)</el-checkbox>
+            <el-checkbox label="kfmix">关键帧融合(KFMix)</el-checkbox>
+            <el-checkbox label="octomap">八叉树地图(Octomap)</el-checkbox>
+            <el-checkbox label="tilemap">瓦片地图(TileMap)</el-checkbox>
+            <el-checkbox label="potree">点云地图(Potree)</el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item>
+          <el-alert type="info" :closable="false" show-icon>
+            <template #title>
+              默认执行全部构建步骤,如只需构建部分步骤请取消选择对应选项
+            </template>
+          </el-alert>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="buildConfigDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleStartBuild">开始构建</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, reactive, onMounted, onUnmounted, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search, Plus, Upload, MapLocation, Menu, Grid, ArrowDown, VideoPause, Close, Check } from '@element-plus/icons-vue'
+import { useMapList, useMapControl } from '@/composables/useMap'
+import { useWebSocket } from '@/composables/useWebSocket'
+import * as mapApi from '@/api/robot/map'
+
+const router = useRouter()
+const route = useRoute()
+
+// 地图列表相关
+const { loading, mapList, fetchMapList, fetchMapThumbnail, renameMap, removeMap, downloadMap } = useMapList()
+
+// 地图控制相关
+const deviceId = ref('ld000001') // TODO: 从配置或选择器获取
+
+// ASM 录制/构建/SLAM 状态管理
+const {
+  isRecording, isBuilding, isSlaming,
+  recordingMapName, buildingMapName, slamMapName, buildingProgress,
+  startRecord, stopRecord,
+  startBuild, stopBuild, completedBuild,
+  startSlam, stopSlam,
+  updateBuildingProgress
+} = useMapControl(deviceId.value)
+
+// WebSocket 连接和消息处理
+const {
+  initWebSocket,
+  disconnect: disconnectWs,
+  subscribeToDevice,
+} = useWebSocket()
+
+// 视图状态
+const viewMode = ref('card')
+const isCompactMode = ref(false)
+const searchKeyword = ref('')
+const statusFilter = ref('all')
+const sortField = ref('updated')
+const currentPage = ref(1)
+const pageSize = ref(12)
+
+// 对话框状态
+const createDialogVisible = ref(false)
+const recordingDialogVisible = ref(false)
+const buildingDialogVisible = ref(false)
+const slamDialogVisible = ref(false)
+const downloadDialogVisible = ref(false)
+const renameDialogVisible = ref(false)
+const buildConfigDialogVisible = ref(false)
+
+// 录制进度
+const recordingProgress = ref(0)
+
+// 表单
+const createFormRef = ref()
+const createForm = reactive({ mapName: '' })
+const createMode = ref('record')
+const createRules = {
+  mapName: [{ required: true, message: '请输入地图名称', trigger: 'blur' }]
+}
+
+// 下载相关
+const downLoadTypes = ref([])
+const availableComponents = ref([])
+const currentDownloadMap = ref('')
+
+// 重命名相关
+const currentRenameMap = ref('')
+const newMapName = ref('')
+
+// 构建配置相关
+const buildConfigMapName = ref('')
+const selectedBuildSteps = ref(['recon', 'kfmix', 'octomap', 'tilemap', 'potree'])
+
+// 等待响应状态
+const waitingForResponse = ref(false)
+
+// 计算属性
+const hasSearchOrFilter = computed(() => searchKeyword.value.trim() || statusFilter.value !== 'all')
+
+const filteredList = computed(() => {
+  let list = [...mapList.value]
+
+  // 搜索过滤
+  if (searchKeyword.value.trim()) {
+    const keyword = searchKeyword.value.toLowerCase()
+    list = list.filter(item => {
+      const name = (item.map || item.mapName || item.name || '').toLowerCase()
+      return name.includes(keyword)
+    })
+  }
+
+  // 状态筛选
+  if (statusFilter.value !== 'all') {
+    list = list.filter(item => (item.state || item.status) === statusFilter.value)
+  }
+
+  return list
+})
+
+const totalCount = computed(() => filteredList.value.length)
+
+const displayedList = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value
+  return filteredList.value.slice(start, start + pageSize.value)
+})
+
+// 状态配置
+const statusConfig = {
+  available: { type: 'success', text: '正常' },
+  unavailable: { type: 'danger', text: '不可用' },
+  building: { type: 'warning', text: '正在建图' },
+  recording: { type: 'warning', text: '正在录制' },
+  slaming: { type: 'success', text: '实时建图' }
+}
+
+function getStatusType(state) {
+  return statusConfig[state]?.type || 'info'
+}
+
+function getStatusText(state) {
+  return statusConfig[state]?.text || state || '未知'
+}
+
+// 格式化文件大小
+function formatFileSize(bytes) {
+  if (!bytes || bytes === 0) return '0 B'
+  const units = ['B', 'KB', 'MB', 'GB', 'TB']
+  const k = 1024
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + units[i]
+}
+
+// 加载数据
+async function loadData() {
+  try {
+    await fetchMapList(deviceId.value)
+
+    // 加载缩略图(仅获取前6个地图的缩略图用于卡片显示)
+    const limit = Math.min(6, mapList.value.length)
+    for (let i = 0; i < limit; i++) {
+      const mapItem = mapList.value[i]
+      const name = mapItem.map || mapItem.mapName || mapItem.name
+      if (name && !mapItem.thumbUrl) {
+        try {
+          const thumbUrl = await fetchMapThumbnail(name)
+          if (thumbUrl) {
+            mapItem.thumbUrl = thumbUrl
+          }
+        } catch (e) {
+          console.warn('获取缩略图失败:', name)
+        }
+      }
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  }
+}
+
+// WebSocket 消息处理
+function handleWsMessage(type, message) {
+  switch (type) {
+    case 'asm':
+      handleAsmMessage(message)
+      break
+    case 'map':
+      // 地图列表更新时刷新
+      loadData()
+      break
+  }
+}
+
+// 处理 ASM 消息
+function handleAsmMessage(message) {
+  const { type, function: funcName, progress, state, data } = message
+
+  if (type === 'asm_progress') {
+    // 进度反馈
+    if (funcName === 'SensorRecorder.start') {
+      // 录制进度
+      recordingProgress.value = progress || 0
+    } else if (funcName === 'MapBuilder.start') {
+      // 构建进度
+      updateBuildingProgress(progress || 0)
+    }
+  } else if (type === 'asm_state') {
+    // 状态反馈
+    waitingForResponse.value = false
+
+    // 开始录制状态反馈
+    if (funcName === 'ASM.sensor_record.start') {
+      if (state === 2) {
+        ElMessage.success(`地图"${recordingMapName.value}"开始录制`)
+      } else if (state === 3) {
+        ElMessage.error('启动录制失败')
+        resetRecordingState()
+      }
+    }
+    // 停止录制状态反馈
+    else if (funcName === 'ASM.sensor_record.stop') {
+      if (state === 2) {
+        ElMessage.success('录制已停止')
+        resetRecordingState()
+        loadData()
+      } else if (state === 3) {
+        ElMessage.error('停止录制失败')
+      }
+    }
+    // 开始构建状态反馈
+    else if (funcName === 'ASM.map_build.start') {
+      if (state === 2) {
+        ElMessage.success(`地图"${buildingMapName.value}"开始构建`)
+        buildingDialogVisible.value = true
+      } else if (state === 3) {
+        ElMessage.error('启动构建失败')
+        resetBuildingState()
+      }
+    }
+    // 停止构建状态反馈
+    else if (funcName === 'ASM.map_build.stop') {
+      if (state === 2) {
+        ElMessage.info('构建已取消')
+        resetBuildingState()
+      } else if (state === 3) {
+        ElMessage.error('取消构建失败')
+      }
+    }
+    // MapBuilder 完成
+    else if (funcName === 'MapBuilder.start') {
+      if (state === 2) {
+        ElMessage.success('地图构建完成')
+        resetBuildingState()
+        completedBuild(buildConfigMapName.value,"MapBuilder.start").then((res) => {
+          loadData()
+        }).catch(() => {
+          ElMessage.error('更新地图状态失败')
+        })
+        
+      } else if (state === 3) {
+        ElMessage.error('地图构建失败')
+        resetBuildingState()
+      }
+    }
+    // 开始实时建图状态反馈
+    else if (funcName === 'ASM.map_slam.start') {
+      if (state === 2) {
+        ElMessage.success(`实时建图"${slamMapName.value}"已开始`)
+        
+        console.log("slamMapName.value1:", slamMapName.value);
+        
+        completedBuild(slamMapName.value,"ASM.map_slam.start").then((res) => {
+          loadData()
+        }).catch(() => {
+          ElMessage.error('更新地图状态失败')
+        })
+      } else if (state === 3) {
+        ElMessage.error('启动实时建图失败')
+        resetSlamState()
+      }
+    }
+    // 停止实时建图状态反馈
+    else if (funcName === 'ASM.map_slam.stop') {
+      if (state === 2) {
+        
+        console.log("slamMapName.value",slamMapName.value);
+        
+        ElMessage.success('实时建图已停止,正在生成地图...')
+        completedBuild(slamMapName.value,"ASM.map_slam.stop").then((res) => {
+          loadData()
+          resetSlamState()
+        }).catch(() => {
+          ElMessage.error('更新地图状态失败')
+        })
+      } else if (state === 3) {
+        ElMessage.error('停止实时建图失败')
+      }
+    }
+  }
+}
+
+// 重置录制状态
+function resetRecordingState() {
+  recordingDialogVisible.value = false
+  recordingProgress.value = 0
+  recordingMapName.value = ''
+}
+
+// 重置构建状态
+function resetBuildingState() {
+  buildingDialogVisible.value = false
+  buildingProgress.value = 0
+  buildingMapName.value = ''
+}
+
+// 重置实时建图状态
+function resetSlamState() {
+  slamDialogVisible.value = false
+  slamMapName.value = ''
+}
+
+// 搜索/筛选/排序
+function handleSearch() {
+  currentPage.value = 1
+}
+
+function handleFilterChange() {
+  currentPage.value = 1
+}
+
+function handleSortChange() {
+  currentPage.value = 1
+}
+
+function handleSizeChange(size) {
+  pageSize.value = size
+  currentPage.value = 1
+}
+
+function handlePageChange(page) {
+  currentPage.value = page
+}
+
+function setViewMode(mode) {
+  viewMode.value = mode
+  localStorage.setItem('map-view-mode', mode)
+}
+
+// 新建地图
+function handleCreate() {
+  createForm.mapName = ''
+  createMode.value = 'record'
+  createDialogVisible.value = true
+}
+
+async function submitCreate() {
+  if (!createForm.mapName.trim()) {
+    ElMessage.warning('请输入地图名称')
+    return
+  }
+
+  createDialogVisible.value = false
+
+  if (createMode.value === 'record') {
+    // 录制模式
+    recordingMapName.value = createForm.mapName
+    waitingForResponse.value = true
+
+    try {
+      await startRecord(createForm.mapName)
+      recordingDialogVisible.value = true
+
+      // 30秒超时
+      setTimeout(() => {
+        if (waitingForResponse.value && !recordingDialogVisible.value) {
+          waitingForResponse.value = false
+          ElMessage.error('启动录制超时,请检查设备连接')
+        }
+      }, 30000)
+
+      await loadData()
+    } catch (e) {
+      waitingForResponse.value = false
+      resetRecordingState()
+    }
+  } else {
+    // 实时建图模式
+    slamMapName.value = createForm.mapName
+    waitingForResponse.value = true
+
+    try {
+      await startSlam(createForm.mapName)
+      slamDialogVisible.value = true
+
+      // 30秒超时
+      setTimeout(() => {
+        if (waitingForResponse.value && !slamDialogVisible.value) {
+          waitingForResponse.value = false
+          ElMessage.error('启动实时建图超时,请检查设备连接')
+        }
+      }, 30000)
+
+      await loadData()
+    } catch (e) {
+      waitingForResponse.value = false
+      resetSlamState()
+    }
+  }
+}
+
+// 实时建图(工具栏按钮)
+async function handleSlam() {
+  try {
+    const { value: mapName } = await ElMessageBox.prompt('请输入地图名称', '实时建图', {
+      confirmButtonText: '开始实时建图',
+      cancelButtonText: '取消',
+      inputPattern: /\S+/
+    })
+
+    slamMapName.value = mapName
+    waitingForResponse.value = true
+
+    await startSlam(mapName)
+    slamDialogVisible.value = true
+
+    setTimeout(() => {
+      if (waitingForResponse.value && !slamDialogVisible.value) {
+        waitingForResponse.value = false
+        ElMessage.error('启动实时建图超时,请检查设备连接')
+      }
+    }, 30000)
+
+    await loadData()
+  } catch (e) {
+    // 用户取消
+  }
+}
+
+// 停止录制
+async function handleStopRecord() {
+  try {
+    await ElMessageBox.confirm('确认停止录制?停止后将无法继续录制。', '停止录制', {
+      type: 'warning',
+      confirmButtonText: '确认停止',
+      cancelButtonText: '继续录制'
+    })
+    
+    waitingForResponse.value = true
+    
+    await stopRecord(recordingMapName.value)
+
+    setTimeout(() => {
+      if (waitingForResponse.value) {
+        waitingForResponse.value = false
+        ElMessage.error('停止录制超时,请检查设备连接')
+      }
+    }, 30000)
+  } catch (e) {
+    // 用户取消
+  }
+}
+
+// 停止构建
+async function handleStopBuild() {
+  try {
+    await ElMessageBox.confirm('确认取消构建?', '取消构建', {
+      type: 'warning',
+      confirmButtonText: '确认取消',
+      cancelButtonText: '继续构建'
+    })
+
+    waitingForResponse.value = true
+    await stopBuild()
+
+    setTimeout(() => {
+      if (waitingForResponse.value) {
+        waitingForResponse.value = false
+        ElMessage.error('取消构建超时,请检查设备连接')
+      }
+    }, 30000)
+  } catch (e) {
+    // 用户取消
+  }
+}
+
+// 停止实时建图
+async function handleStopSlam() {
+  try {
+    await ElMessageBox.confirm(
+      '确认停止实时建图?停止后将自动生成地图文件,这可能需要较长时间。',
+      '停止实时建图',
+      {
+        type: 'warning',
+        confirmButtonText: '确认停止',
+        cancelButtonText: '继续建图'
+      }
+    )
+
+    waitingForResponse.value = true
+    await stopSlam()
+
+    setTimeout(() => {
+      if (waitingForResponse.value) {
+        waitingForResponse.value = false
+        ElMessage.warning('停止实时建图超时,但地图可能仍在后台生成中,请稍后查看地图列表')
+      }
+    }, 60000)
+  } catch (e) {
+    // 用户取消
+  }
+}
+
+// 卡片点击
+function handleCardClick(item) {
+  // 默认导航到编辑页
+  handleEdit(item)
+}
+
+// 导航
+function handleNavigation(item) {
+  const name = item.map || item.mapName || item.name
+  console.log("item",name);
+  router.push(`/map/maplist/navigation/${encodeURIComponent(name)}`)
+}
+
+// 编辑
+function handleEdit(item) {
+  const name = item.map || item.mapName || item.name
+  router.push(`/map/maplist/edit/${encodeURIComponent(name)}`)
+}
+
+// 标定
+function handleCalibrate(item) {
+  const name = item.map || item.mapName || item.name
+  router.push(`/map/maplist/calibration/${encodeURIComponent(name)}`)
+}
+
+// 更多操作
+async function handleCommand(item, cmd) {
+  const name = item.map || item.mapName || item.name
+
+  switch (cmd) {
+    case 'rename':
+      currentRenameMap.value = name
+      newMapName.value = name
+      renameDialogVisible.value = true
+      break
+
+    case 'download':
+      try {
+        const res = await mapApi.getMapComponents(name)
+        if (res.code === 200 && res.data) {
+          availableComponents.value = res.data
+          downLoadTypes.value = res.data.map(c => c.label)
+          currentDownloadMap.value = name
+          downloadDialogVisible.value = true
+        }
+      } catch (e) {
+        ElMessage.error('获取组件列表失败')
+      }
+      break
+
+    case 'build':
+      // 检查地图状态
+      if (item.state === 'recording') {
+        ElMessage.warning('地图正在录制中,请先停止录制后再构建')
+        return
+      }
+      if (item.state === 'building') {
+        ElMessage.warning('地图正在构建中')
+        return
+      }
+      if (item.state === 'slaming') {
+        ElMessage.warning('地图正在实时建图中')
+        return
+      }
+      buildConfigMapName.value = name
+      selectedBuildSteps.value = ['recon', 'kfmix', 'octomap', 'tilemap', 'potree']
+      buildConfigDialogVisible.value = true
+      break
+
+    case 'calibrate':
+      handleCalibrate(item)
+      break
+
+    case 'delete':
+      try {
+        await ElMessageBox.confirm(`确定要删除地图"${name}"吗?此操作不可恢复`, '删除确认', {
+          type: 'warning'
+        })
+        await removeMap(name)
+      } catch (e) {
+        // 用户取消
+      }
+      break
+  }
+}
+
+// 开始构建地图
+async function handleStartBuild() {
+  if (!buildConfigMapName.value) {
+    ElMessage.warning('请选择要构建的地图')
+    return
+  }
+
+  buildConfigDialogVisible.value = false
+  buildingMapName.value = buildConfigMapName.value
+  waitingForResponse.value = true
+
+  try {
+    await startBuild(buildConfigMapName.value)
+    buildingDialogVisible.value = true
+
+    setTimeout(() => {
+      if (waitingForResponse.value && !buildingDialogVisible.value) {
+        waitingForResponse.value = false
+        ElMessage.error('启动构建超时,请检查设备连接')
+      }
+    }, 30000)
+
+    await loadData()
+  } catch (e) {
+    waitingForResponse.value = false
+    resetBuildingState()
+  }
+}
+
+// 重命名
+async function submitRename() {
+  if (!newMapName.value.trim()) {
+    ElMessage.warning('请输入新名称')
+    return
+  }
+
+  try {
+    await renameMap(currentRenameMap.value, newMapName.value)
+    renameDialogVisible.value = false
+    await loadData()
+  } catch (e) {
+    // 错误已在composable中处理
+  }
+}
+
+// 下载
+async function submitDownload() {
+  try {
+    await downloadMap(currentDownloadMap.value, downLoadTypes.value)
+    downloadDialogVisible.value = false
+  } catch (e) {
+    // 错误已在composable中处理
+  }
+}
+
+// 导入
+function handleImport() {
+  ElMessage.info('导入功能开发中...')
+}
+
+// 初始化
+onMounted(() => {
+  // 恢复视图模式
+  const savedMode = localStorage.getItem('map-view-mode')
+  if (savedMode) {
+    viewMode.value = savedMode
+  }
+
+  // 初始化 WebSocket
+  initWebSocket({
+    deviceId: deviceId.value,
+    onMessage: handleWsMessage,
+    onConnect: () => {
+      console.log('[MapList] WebSocket已连接')
+    },
+    onDisconnect: () => {
+      console.log('[MapList] WebSocket已断开')
+    }
+  })
+
+  loadData()
+})
+
+// 组件卸载时断开 WebSocket
+onUnmounted(() => {
+  disconnectWs()
+})
+
+// 监听录制状态,自动显示/隐藏对话框
+watch(isRecording, (newVal) => {
+  if (newVal && recordingMapName.value) {
+    recordingDialogVisible.value = true
+  }
+})
+
+watch(isBuilding, (newVal) => {
+  if (!newVal && buildingMapName.value) {
+    buildingDialogVisible.value = false
+  }
+})
+
+watch(isSlaming, (newVal) => {
+  if (!newVal && slamMapName.value) {
+    slamDialogVisible.value = false
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.map-list-container {
+  padding: 16px;
+  min-height: calc(100vh - 140px);
+}
+
+.toolbar-section {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 16px;
+  padding: 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  margin-bottom: 16px;
+}
+
+.toolbar-filters {
+  display: flex;
+  gap: 12px;
+
+  .search-input { width: 200px; }
+  .status-filter, .sort-select { width: 140px; }
+}
+
+.toolbar-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.main-content {
+  min-height: 400px;
+}
+
+.card-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 16px;
+
+  &.compact {
+    gap: 12px;
+    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+  }
+}
+
+.map-card {
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    transform: translateY(-2px);
+  }
+}
+
+.card-thumbnail {
+  height: 160px;
+  background: var(--el-fill-color-light);
+  border-radius: 4px;
+  overflow: hidden;
+  margin-bottom: 12px;
+
+  img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+
+  .thumbnail-placeholder {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: var(--el-text-color-placeholder);
+  }
+}
+
+.card-info {
+  margin-bottom: 12px;
+
+  .map-name {
+    margin: 0 0 8px 0;
+    font-size: 16px;
+    font-weight: 500;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+
+.card-actions {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.empty-state {
+  grid-column: 1 / -1;
+  text-align: center;
+  padding: 60px 20px;
+
+  .empty-icon {
+    font-size: 60px;
+    color: var(--el-text-color-placeholder);
+    margin-bottom: 16px;
+  }
+
+  h3 {
+    margin: 0 0 8px 0;
+    color: var(--el-text-color-primary);
+  }
+
+  p {
+    margin: 0 0 16px 0;
+    color: var(--el-text-color-secondary);
+  }
+}
+
+.map-table {
+  background: var(--el-bg-color);
+  border-radius: 8px;
+}
+
+.pagination-section {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16px;
+}
+
+.dialog-content {
+  padding: 8px 0;
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+
+  .label {
+    width: 100px;
+    color: var(--el-text-color-regular);
+  }
+
+  .value {
+    flex: 1;
+  }
+}
+
+.progress-wrapper {
+  margin-top: 8px;
+  padding: 0 4px;
+}
+</style>

+ 4280 - 0
src/views/map/maplist/navigation.vue

@@ -0,0 +1,4280 @@
+<template>
+  <div class="navigation-container">
+    <div class="map-stage" ref="mapStage">
+      <div class="main-content">
+        <!-- 当前操作类型标记 -->
+        <div class="hand-ment-mark">
+          <el-tag type="danger" effect="dark" size="small" v-if="nowHandMenu">{{ nowHandMenu }}</el-tag>
+        </div>
+
+        <!-- 障碍物警告提示 -->
+        <div class="obstacle-warning" v-if="obstacleWarning.show" :class="'obstacle-' + obstacleWarning.level">
+          <div class="warning-content">
+            <div class="warning-icon">
+              <i class="el-icon-warning-outline" v-if="obstacleWarning.level === 'warning'"></i>
+              <i class="el-icon-error" v-if="obstacleWarning.level === 'danger'"></i>
+              <i class="el-icon-info" v-if="obstacleWarning.level === 'info'"></i>
+            </div>
+            <div class="warning-text">
+              <div class="warning-title">障碍物检测</div>
+              <div class="warning-message">{{ obstacleWarning.message }}</div>
+              <div class="obstacle-distances" v-if="obstacleData.left > 0 || obstacleData.front > 0 || obstacleData.right > 0">
+                <span v-if="obstacleData.left > 0" class="distance-item">
+                  左: {{ obstacleData.left.toFixed(1) }}m
+                </span>
+                <span v-if="obstacleData.front > 0" class="distance-item">
+                  前: {{ obstacleData.front.toFixed(1) }}m
+                </span>
+                <span v-if="obstacleData.right > 0" class="distance-item">
+                  右: {{ obstacleData.right.toFixed(1) }}m
+                </span>
+              </div>
+            </div>
+            <div class="warning-close" @click="closeObstacleWarning">
+              <i class="el-icon-close"></i>
+            </div>
+          </div>
+        </div>
+
+        <!-- 遇障停车提示(基于run_state) -->
+        <div class="obstacle-stop-warning" v-if="obstacleStopState.showWarning">
+          <div class="stop-warning-content">
+            <div class="countdown-circle">
+              <div class="countdown-number">
+                {{ obstacleStopState.replanTimeLeft > 999 ? '999+' : obstacleStopState.replanTimeLeft }}
+              </div>
+              <div class="countdown-label">秒</div>
+            </div>
+            <div class="stop-warning-text">
+              <div class="stop-warning-title">遇障停车</div>
+              <div class="stop-warning-message">检测到前方障碍物,系统将自动重规划路径</div>
+              <div class="stop-warning-state">当前状态: {{ obstacleStopState.runStateText }}</div>
+            </div>
+            <div class="stop-warning-actions">
+              <el-button
+                type="primary"
+                size="small"
+                @click="handleReplanRequest"
+                :disabled="obstacleStopState.isReplanning"
+              >
+                {{ obstacleStopState.isReplanning ? '重规划中...' : '立即重规划' }}
+              </el-button>
+            </div>
+          </div>
+        </div>
+
+        <!-- 地图组件 -->
+        <OlMap 
+          ref="olmap" 
+          :width="olWidth + 'px'" 
+          :height="olHeight + 'px'" 
+          backgroundColor="#F5F5F5" 
+          :mapName="mapName"
+          :pointSwitch="settingParams.pointId" 
+          :baseLayerShow="settingParams.baseMap" :robotPoseData="laserPositionData"
+          :pointSelectionEnabled="selectPointMode" :poseInitEnable="initPoseMode" @addNowPoint="addNowPoint"
+          :isRobotFollow="settingParams.follow" @initNavigationResult="initNavigationResult"
+          :showPointcloud="settingParams.pointCloud"
+          :showRoadNetwork="settingParams.showRoadNetwork"
+          :showDefaultControls="false"></OlMap>
+
+        <!-- 左侧工具条浮层 -->
+        <MapToolbar
+          class="nav-toolbar"
+          preset="nav"
+          :selectedKey="selectedKey"
+          :hasRobotPosition="true"
+          :isConnected="true"
+          :isBusy="false"
+          :isFullscreen="isFullscreen"
+          @zoom-in="onZoomIn"
+          @zoom-out="onZoomOut"
+          @center-robot="onCenterRobot"
+          @toggle-fullscreen="onToggleFullscreen"
+          @confirm-init="handleConfirmInit"
+          @confirm-reboot="handleConfirmReboot"
+          @confirm-stop="handleConfirmStop"
+        />
+
+        <!-- 右侧信息面板浮层 -->
+        <RightPanel
+          mode="nav"
+          panelType="nav"
+          :overlay="true"
+          v-model:visible="rightVisible"
+          :realtime-info="realtimeInfo"
+          :waypoint-list="waypoints"
+          :task-list="tasks"
+          :setting-params="settingParams"
+          :emergency-stop-enabled="emergencyStopEnabled"
+          :navigation-stack-status="navigationStackStatus"
+          :current-navigation-task="currentNavigationTask"
+          :navigation-status="navigationStatus"
+          :is-navigating="isNavigating"
+          @wp-select="onWpSelect"
+          @wp-send="onWpSend"
+          @wp-create="onWpCreate"
+          @wp-edit="onWpEdit"
+          @wp-remove="onWpRemove"
+          @wp-move-up="onWpMoveUp"
+          @wp-move-down="onWpMoveDown"
+          @wp-batch-remove="onWpBatchRemove"
+          @wp-goto="onWpGoto"
+          @wp-goto-single="onWpGotoSingle"
+          @wp-create-task="onWpCreateTask"
+          @wp-selection-change="onWpSelectionChange"
+          @map-select-mode-change="onMapSelectModeChange"
+          @task-view="onTaskView"
+          @task-edit="onTaskEdit"
+          @task-start="onTaskStart"
+          @task-pause="onTaskPause"
+          @task-resume="onTaskResume"
+          @task-stop="onTaskStop"
+          @task-remove="onTaskRemove"
+          @setting-change="onSettingChange"
+          @emergency-stop-release="executeEmergencyStopRelease"
+          @navigation-pause="handleNavigationPause"
+          @navigation-resume="handleNavigationResume"
+          @navigation-stop="handleNavigationStop"
+        />
+      </div>
+
+      <!-- 目标点编辑对话框 -->
+      <el-dialog
+        title="目标点编辑"
+        v-model="pointEditDiaShow"
+        width="480px"
+        @close="clearActionDia"
+        class="waypoint-edit-dialog"
+        :close-on-click-modal="false"
+        center
+      >
+        <div class="dialog-content">
+          <el-form :model="pointEditData" label-width="90px" size="small" class="waypoint-form">
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-location-outline"></i>
+                位置信息
+              </h4>
+              <el-form-item label="X坐标(m)">
+                <el-input v-model="pointEditData.x" placeholder="请输入X坐标值" class="coordinate-input">
+                </el-input>
+              </el-form-item>
+              <el-form-item label="Y坐标(m)">
+                <el-input v-model="pointEditData.y" placeholder="请输入Y坐标值" class="coordinate-input">
+                </el-input>
+              </el-form-item>
+            </div>
+
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-guide"></i>
+                路径配置
+              </h4>
+              <el-form-item label="规划类型">
+                <el-select v-model="pointEditData.type" placeholder="请选择路径类型" style="width: 100%">
+                  <el-option v-for="item in planOptions" :key="item.value" :label="item.label" :value="item.value">
+                    <span style="float: left">{{ item.label }}</span>
+                    <span style="float: right; color: #8492a6; font-size: 13px">{{ item.value === 0 ? '自由规划' : '路网约束' }}</span>
+                  </el-option>
+                </el-select>
+              </el-form-item>
+            </div>
+
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-setting"></i>
+                动作配置
+                <el-button type="text" size="small" @click="appendActionMenu" class="add-action-btn">
+                  <i class="el-icon-plus"></i><span>添加动作</span>
+                </el-button>
+              </h4>
+              <div class="action-list">
+                <div v-for="(item, index) in pointEditData.actionMenuList" :key="index" class="action-item">
+                  <div class="action-header">
+                    <span class="action-index">{{ index + 1 }}</span>
+                    <el-select v-model="item.value" placeholder="请选择动作" size="small" @change="changeAction(index)" style="flex: 1;">
+                      <el-option v-for="option in actionOptions" :key="option.value" :label="option.label" :value="option.value"></el-option>
+                    </el-select>
+                    <el-button v-if="index > 0" type="text" size="small" @click="removeActionMenu(index)" class="remove-btn">
+                      <i class="el-icon-close"></i>
+                    </el-button>
+                  </div>
+                  <div v-if="'other' in item" class="action-params">
+                    <el-input v-model="item.other" placeholder="等待时间" size="small" style="width: 120px;">
+                      <template #append>秒</template>
+                    </el-input>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-form>
+        </div>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button @click="pointEditDiaShow = false">取 消</el-button>
+            <el-button type="primary" @click="submitEditPoint">
+              <i class="el-icon-check"></i> 保存修改
+            </el-button>
+          </span>
+        </template>
+      </el-dialog>
+
+      <!-- 创建任务对话框 -->
+      <el-dialog
+        title="创建任务"
+        v-model="taskGenerateDiaShow"
+        width="480px"
+        @close="closeTaskGenerate"
+        class="task-create-dialog"
+        :close-on-click-modal="false"
+        center
+      >
+        <div class="dialog-content">
+          <el-form :model="generateTaskParam" label-width="90px" size="small" class="task-form">
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-s-order"></i>
+                任务信息
+              </h4>
+              <el-form-item label="任务名称">
+                <el-input v-model="generateTaskParam.taskName" placeholder="请输入任务名称" class="task-input">
+                </el-input>
+              </el-form-item>
+              <el-form-item label="执行次数">
+                <el-input-number
+                  v-model="generateTaskParam.count"
+                  controls-position="right"
+                  :min="1"
+                  :max="100"
+                  class="task-input-number"
+                  style="width: 100%"
+                ></el-input-number>
+              </el-form-item>
+            </div>
+
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-time"></i>
+                执行计划
+              </h4>
+              <el-form-item label="开始时间">
+                <el-time-picker
+                  v-model="generateTaskParam.time"
+                  format="HH:mm"
+                  value-format="HH:mm:ss"
+                  :picker-options="{
+                    selectableRange: '00:00:00 - 23:59:59'
+                  }"
+                  placeholder="选择时间"
+                  class="task-time-picker"
+                  style="width: 100%"
+                >
+                </el-time-picker>
+              </el-form-item>
+              <el-form-item label="执行日期">
+                <el-checkbox-group v-model="generateTaskParam.date" class="task-date-group">
+                  <el-checkbox label="1">周一</el-checkbox>
+                  <el-checkbox label="2">周二</el-checkbox>
+                  <el-checkbox label="3">周三</el-checkbox>
+                  <el-checkbox label="4">周四</el-checkbox>
+                  <el-checkbox label="5">周五</el-checkbox>
+                  <el-checkbox label="6">周六</el-checkbox>
+                  <el-checkbox label="7">周日</el-checkbox>
+                </el-checkbox-group>
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button @click="closeTaskGenerate()">取 消</el-button>
+            <el-button type="primary" @click="submitTaskGenerate">
+              <i class="el-icon-check"></i> 创建任务
+            </el-button>
+          </span>
+        </template>
+      </el-dialog>
+
+      <!-- 任务编辑对话框 -->
+      <el-dialog
+        title="编辑任务"
+        v-model="taskEditDiaShow"
+        width="480px"
+        @close="closeTaskEdit"
+        class="task-create-dialog"
+        :close-on-click-modal="false"
+        center
+      >
+        <div class="dialog-content">
+          <el-form :model="editTaskParam" label-width="90px" size="small" class="task-form">
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-s-order"></i>
+                任务信息
+              </h4>
+              <el-form-item label="任务名称">
+                <el-input v-model="editTaskParam.taskName" placeholder="请输入任务名称" class="task-input">
+                </el-input>
+              </el-form-item>
+              <el-form-item label="执行次数">
+                <el-input-number
+                  v-model="editTaskParam.count"
+                  controls-position="right"
+                  :min="1"
+                  :max="100"
+                  class="task-input-number"
+                  style="width: 100%"
+                ></el-input-number>
+              </el-form-item>
+            </div>
+
+            <div class="form-section">
+              <h4 class="section-title">
+                <i class="el-icon-time"></i>
+                执行计划
+              </h4>
+              <el-form-item label="开始时间">
+                <el-time-picker
+                  v-model="editTaskParam.time"
+                  format="HH:mm"
+                  value-format="HH:mm:ss"
+                  :picker-options="{
+                    selectableRange: '00:00:00 - 23:59:59'
+                  }"
+                  placeholder="选择时间"
+                  class="task-time-picker"
+                  style="width: 100%"
+                >
+                </el-time-picker>
+              </el-form-item>
+              <el-form-item label="执行日期">
+                <el-checkbox-group v-model="editTaskParam.date" class="task-date-group">
+                  <el-checkbox label="1">周一</el-checkbox>
+                  <el-checkbox label="2">周二</el-checkbox>
+                  <el-checkbox label="3">周三</el-checkbox>
+                  <el-checkbox label="4">周四</el-checkbox>
+                  <el-checkbox label="5">周五</el-checkbox>
+                  <el-checkbox label="6">周六</el-checkbox>
+                  <el-checkbox label="7">周日</el-checkbox>
+                </el-checkbox-group>
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button @click="closeTaskEdit()">取 消</el-button>
+            <el-button type="primary" @click="submitTaskEdit">
+              <i class="el-icon-check"></i> 保存修改
+            </el-button>
+          </span>
+        </template>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, onActivated, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
+import OlMap from "@/components/OlMap"
+import MapToolbar from "../components/shared/MapToolbar.vue"
+import RightPanel from "../components/shared/RightPanel.vue"
+import { FullscreenOperations, RobotPositionUtils } from "@/utils/map-operations"
+import { listTask, addTask, delTask, getTask, updateTask, updateTaskStatus } from "@/api/map/task"
+import { initWebSocket, disconnect, sendMessage } from "@/utils/websocket"
+import {
+  initPose,
+  initPoseByNid,
+  startNavigation,
+  stopNavigation,
+  restartNavigation,
+  startNavStandard,
+  gotoTargetByCoord,
+  gotoTarget,
+  requestPlanning,
+  startTask,
+  pauseTask,
+  resumeTask,
+  cancelTask,
+  replan,
+  emergencyStop,
+  releaseEmergencyStop
+} from "@/api/robot/mqtt"
+
+// 获取路由
+const route = useRoute()
+
+// WebSocket设备ID
+const wsDeviceId = ref('ld000001')
+
+// Refs
+const mapStage = ref(null)
+const olmap = ref(null)
+
+const nowHandMenu = ref('')
+const mapName = ref(route.params.mapName || '')
+const laserPositionData = reactive({
+  x: 0,
+  y: 0,
+  angle: 0
+})
+
+// 设置相关
+const settingDrawer = ref(false)
+const pointDrawer = ref(false)
+const taskDrawer = ref(false)
+const pointEditDiaShow = ref(false)
+const taskGenerateDiaShow = ref(false)
+const taskEditDiaShow = ref(false)
+const taskViewDiaShow = ref(false)
+
+const selectPointMode = ref(false)
+const initPoseMode = ref(false)
+
+const settingParams = reactive({
+  pointCloud: false,
+  baseMap: true,
+  pointId: false,
+  follow: false,
+  network: false,
+  showRoadNetwork: true
+})
+
+const single = ref(true)
+const multiple = ref(true)
+const taskDataList = ref([])
+
+// 目标点编辑数据
+const pointEditData = reactive({
+  id: '',
+  x: '',
+  y: '',
+  type: '',
+  actionMenuList: []
+})
+
+// 生成任务参数
+const generateTaskParam = reactive({
+  taskId: '',
+  taskName: '',
+  count: 1,
+  time: '',
+  date: [],
+  selectedWaypoints: []
+})
+
+// 编辑任务参数
+const editTaskParam = reactive({
+  originalTaskName: '',
+  taskId: '',
+  taskName: '',
+  count: 1,
+  time: '',
+  date: [],
+  selectedWaypoints: []
+})
+
+const taskViewData = ref({})
+const pointList = ref([])
+const pointIds = ref([])
+
+// 选项
+const planOptions = ref([
+  { label: '自由路径', value: 0 },
+  { label: '路网路径', value: 1 }
+])
+
+const actionOptions = ref([
+  { label: '原地等待', value: 0 },
+  { label: '开始录制', value: 1 },
+  { label: '结束录制', value: 2 },
+  { label: '添加建图轨迹', value: 3 },
+  { label: '挂钩挂载', value: 4 },
+  { label: '挂钩卸载', value: 5 }
+])
+
+const olWidth = ref(0)
+const olHeight = ref(0)
+
+const panelVisible = ref(true)
+const rightVisible = ref(true)
+const activeTab = ref('info')
+const lastTab = ref('info')
+
+const mockTasks = ref([
+  { id: 1, name: '巡检任务A', nodes: 5, status: 'idle' },
+  { id: 2, name: '运输任务B', nodes: 3, status: 'running' },
+  { id: 3, name: '清扫任务C', nodes: 8, status: 'paused' }
+])
+
+const realtimeInfo = reactive({
+  currentMap: route.params.mapName || 'Unknown',
+  currentTask: '',
+  speed: '',
+  speedCommand: '',
+  coordinates: '',
+  heading: '',
+  totalDistance: '',
+  registrationError: '',
+  batteryLevel: '',
+  batteryDetails: {
+    capacity: 0,
+    temperature: 0,
+    voltage: 0,
+    current: 0,
+    charging: false,
+    other: []
+  }
+})
+
+const robotPoseData = reactive({
+  x: 1.813,
+  y: -63.931,
+  angle: 0.000
+})
+
+const isConnected = ref(true)
+const isBusy = ref(false)
+const isFullscreen = ref(false)
+
+const navigationStackStatus = ref('unknown')
+const emergencyStopEnabled = ref(false)
+
+const fullscreenCleanup = ref(null)
+
+// 目标点相关
+const selectedWaypointIds = ref([])
+const waypointSingle = ref(true)
+const waypointMultiple = ref(true)
+
+// 导航相关
+const waypoints = ref([])
+const currentNavigationTask = ref(null)
+const isNavigating = ref(false)
+const navigationStatus = ref('idle')
+const lastGotoRequest = ref(null)
+
+// 任务相关
+const currentExecutingTask = ref(null)
+const taskQueue = ref([])
+const isTaskExecuting = ref(false)
+const taskExecutionStatus = ref('idle')
+const currentTaskWaypointIndex = ref(0)
+const taskRealtimeInfo = reactive({
+  odom: { total: 0, remain: 0 },
+  time: { total: 0, remain: 0, duration: 0 },
+  driveMode: 'auto',
+  autoReady: true
+})
+const lastTaskExecRequest = ref(null)
+const tasks = ref([])
+
+const currentViewTaskWaypointIds = ref([])
+const taskViewAutoCloseTimer = ref(null)
+const taskListTimer = ref(null)
+
+// 定时任务
+const scheduledTasksToday = ref([])
+const scheduledTasksExecuted = ref([])
+const currentScheduledTask = ref(null)
+const lastTaskStartTime = ref({})
+const autoExecuteTimer = ref(null)
+const autoExecuteEnabled = ref(true)
+
+// 障碍物检测
+const obstacleData = reactive({
+  left: 0,
+  front: 0,
+  right: 0,
+  timestamp: 0
+})
+
+const obstacleWarning = reactive({
+  show: false,
+  level: 'info',
+  message: '',
+  direction: '',
+  distance: 0,
+  autoHideTimer: null
+})
+
+const obstacleSettings = reactive({
+  warningDistance: 3.0,
+  dangerDistance: 1.0,
+  autoHideDelay: 5000,
+  enableWarning: true
+})
+
+const obstacleStopState = reactive({
+  counter: 0,
+  replanTimeLeft: 0,
+  showWarning: false,
+  isReplanning: false,
+  runState: 0,
+  runStateText: '未知'
+})
+
+// Computed
+const selectedKey = computed(() => {
+  if (initPoseMode.value) return 'init-pose'
+  return ''
+})
+
+const robotPosition = computed(() => {
+  if (robotPoseData && (robotPoseData.x !== 0 || robotPoseData.y !== 0)) {
+    return [robotPoseData.x, robotPoseData.y]
+  }
+  return null
+})
+
+const hasValidRobotPosition = computed(() => {
+  return !!robotPosition.value
+})
+
+const isMapReady = computed(() => {
+  return !!getMapInstance()
+})
+
+const navigationStatusText = computed(() => {
+  switch(navigationStatus.value) {
+    case 'idle': return '空闲'
+    case 'planning': return '规划中'
+    case 'navigating': return '导航中'
+    case 'arrived': return '已到达'
+    case 'failed': return '失败'
+    default: return '未知'
+  }
+})
+
+const canStartNavigation = computed(() => {
+  return !isNavigating.value && navigationStatus.value !== 'planning'
+})
+
+const currentNavigationTarget = computed(() => {
+  if (!currentNavigationTask.value) return null
+  const waypoint = currentNavigationTask.value.waypoint
+  return `${waypoint.name || '目标点' + waypoint.id} (${waypoint.x}, ${waypoint.y})`
+})
+
+const taskExecutionStatusText = computed(() => {
+  switch(taskExecutionStatus.value) {
+    case 'idle': return '空闲'
+    case 'executing': return '执行中'
+    case 'paused': return '暂停'
+    case 'completed': return '已完成'
+    case 'failed': return '失败'
+    case 'cancelled': return '已取消'
+    default: return '未知'
+  }
+})
+
+const currentTaskDescription = computed(() => {
+  if (!currentExecutingTask.value) return null
+  const task = currentExecutingTask.value
+  const progress = `${currentTaskWaypointIndex.value}/${task.points?.length || 0}`
+  return `${task.taskName} (${progress})`
+})
+
+const canStartNewTask = computed(() => {
+  return !isTaskExecuting.value && !isNavigating.value
+})
+
+const taskProgressInfo = computed(() => {
+  if (!currentExecutingTask.value || !taskRealtimeInfo) return null
+  return {
+    taskName: currentExecutingTask.value.taskName,
+    waypointProgress: `${currentTaskWaypointIndex.value}/${currentExecutingTask.value.points?.length || 0}`,
+    totalDistance: `${(taskRealtimeInfo.odom.total / 1000).toFixed(2)}km`,
+    remainDistance: `${(taskRealtimeInfo.odom.remain / 1000).toFixed(2)}km`,
+    totalTime: formatSeconds(taskRealtimeInfo.time.total),
+    remainTime: formatSeconds(taskRealtimeInfo.time.remain),
+    duration: formatSeconds(taskRealtimeInfo.time.duration),
+    driveMode: taskRealtimeInfo.driveMode === 'auto' ? '自动' : '手动',
+    autoReady: taskRealtimeInfo.autoReady
+  }
+})
+
+const todayScheduleInfo = computed(() => {
+  const pendingTasks = scheduledTasksToday.value.filter(
+    taskName => !scheduledTasksExecuted.value.includes(taskName) &&
+    (!currentScheduledTask.value || currentScheduledTask.value.name !== taskName)
+  )
+  return {
+    total: scheduledTasksToday.value.length,
+    executed: scheduledTasksExecuted.value.length,
+    running: currentScheduledTask.value ? 1 : 0,
+    pending: pendingTasks.length,
+    currentRunning: currentScheduledTask.value,
+    pendingList: pendingTasks
+  }
+})
+
+const hasScheduleRunning = computed(() => {
+  return currentScheduledTask.value !== null
+})
+
+const scheduledTasksPending = computed(() => {
+  return scheduledTasksToday.value.filter(
+    taskName => !scheduledTasksExecuted.value.includes(taskName) &&
+    (!currentScheduledTask.value || currentScheduledTask.value.name !== taskName)
+  )
+})
+
+// Watch
+watch(() => route.params.mapName, (newMapName, oldMapName) => {
+  console.log("mapName变化检测:", { newMapName, oldMapName });
+  
+  if (newMapName && newMapName !== mapName.value) {
+    console.log(`路由变化,更新mapName: ${oldMapName} -> ${newMapName}`)
+    mapName.value = newMapName
+  }
+})
+
+watch(panelVisible, () => {
+  nextTick(() => {
+    updateOlCss()
+  })
+})
+
+watch(selectPointMode, (newVal) => {
+  if (newVal) {
+    initPoseMode.value = false
+  }
+})
+
+watch(initPoseMode, (newVal) => {
+  if (newVal) {
+    selectPointMode.value = false
+  }
+})
+
+watch(() => realtimeInfo.coordinates, (newCoordinates) => {
+  if (newCoordinates) {
+    const position = RobotPositionUtils.parseCoordinates(newCoordinates)
+    if (position) {
+      robotPoseData.x = position.x
+      robotPoseData.y = position.y
+    }
+  }
+}, { immediate: true })
+
+// 生命周期
+onMounted(() => {
+  console.log("route.params.mapName",route.params.mapName);
+  
+  // 使用 nextTick 确保 DOM 尺寸已计算
+  nextTick(() => {
+    updateOlCss()
+    // 地图容器尺寸初始化后再次调用确保生效
+    setTimeout(() => {
+      updateOlCss()
+      // 调用 OlMap 组件的 updateMapSize 确保地图正确初始化
+      if (olmap.value && olmap.value.updateMapSize) {
+        olmap.value.updateMapSize()
+      }
+      const map = getMapInstance()
+      if (map) {
+        map.updateSize()
+      }
+    }, 200)
+  })
+  
+  window.addEventListener('resize', updateOlCss)
+
+  // 初始化WebSocket连接
+  initWebSocketConnection()
+
+  loadTaskList()
+  startTaskListTimer()
+
+  fullscreenCleanup.value = FullscreenOperations.addFullscreenListener((fsIsFullscreen) => {
+    isFullscreen.value = fsIsFullscreen
+    nextTick(() => {
+      updateOlCss()
+      const map = getMapInstance()
+      if (map) {
+        map.updateSize()
+      }
+    })
+  })
+
+  setInterval(() => {
+    calculateTodayScheduledTasks()
+  }, 60 * 60 * 1000)
+
+  startAutoExecuteTimer()
+})
+
+onActivated(() => {
+  console.log('导航页面被激活')
+
+  const newMapName = route.params.mapName || ''
+  console.log("mapName变化检测",route.params.mapName);
+  
+  if (mapName.value !== newMapName) {
+    console.log(`更新mapName: ${mapName.value} -> ${newMapName}`)
+    mapName.value = newMapName
+  }
+
+  nextTick(() => {
+    updateOlCss()
+    
+    // 调用 OlMap 组件的 updateMapSize 确保地图正确初始化
+    if (olmap.value && olmap.value.updateMapSize) {
+      olmap.value.updateMapSize()
+    }
+
+    const map = getMapInstance()
+    if (map) {
+      setTimeout(() => {
+        map.updateSize()
+        console.log('地图尺寸已更新')
+      }, 100)
+    }
+
+    if (olmap.value && olmap.value.refresh) {
+      setTimeout(() => {
+        olmap.value.refresh()
+      }, 150)
+    }
+  })
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', updateOlCss)
+
+  if (fullscreenCleanup.value) {
+    fullscreenCleanup.value()
+  }
+
+  if (taskViewAutoCloseTimer.value) {
+    clearTimeout(taskViewAutoCloseTimer.value)
+  }
+
+  stopTaskListTimer()
+  stopAutoExecuteTimer()
+
+  if (obstacleWarning.autoHideTimer) {
+    clearTimeout(obstacleWarning.autoHideTimer)
+  }
+
+  // 断开WebSocket连接
+  disconnect()
+})
+
+// 方法
+const goto = () => {
+  // 原有功能已移至其他方法
+}
+
+// WebSocket消息处理
+const handleWsMessage = (type, message) => {
+  console.log("主题:",type);
+  switch (type) {
+    case 'pose':
+      handleLaserPose(message)
+      break
+    case 'task':
+      handleTaskRealtimeInfoFromWs(message)
+      break
+    case 'trajectory':
+      handleTrajectoryDataFromWs(message)
+      break
+    case 'arrive':
+      handleArriveEventFromWs(message)
+      break
+    case 'navigation':
+      handleNavigationResponseFromWs(message)
+      break
+    case 'planning':
+      handlePlanningResponseFromWs(message)
+      break
+    case 'raw':
+      // 处理通过WebSocket转发的原始数据
+      if (message.topic && message.topic.includes('battery')) {
+        handleBatteryDataFromRaw(message)
+      }
+      break
+    case 'asm':
+      handleAsmResponseFromWs(message)
+      break
+    case 'battery':
+      // 处理电池信息
+      handleBatteryInfoFromWs(message)
+      break
+  }
+}
+
+// 初始化WebSocket连接
+const initWebSocketConnection = () => {
+  initWebSocket({
+    // deviceId: wsDeviceId.value,
+    onConnect: () => {
+      console.log('[Navigation] WebSocket连接成功')
+    },
+    onDisconnect: () => {
+      console.log('[Navigation] WebSocket连接断开')
+    },
+    onMessage: (type, message) => {
+      handleWsMessage(type, message)
+    },
+    onError: (error) => {
+      console.error('[Navigation] WebSocket错误:', error)
+    }
+  })
+}
+
+const handleInitReply = (message) => {
+  // 初始化回复处理
+}
+
+// 通过WebSocket发送消息的辅助函数
+const sendWsMessage = (topic, data) => {
+  const destination = `${topic}`
+  sendMessage(destination, data)
+}
+
+// WebSocket版本的位姿消息处理
+const handleLaserPoseFromWs = (message) => {
+  console.log("接受到pose消息",message);
+  
+  try {
+    const pose = message.data || message
+    const { xyz, rpy } = pose
+
+    const oldX = laserPositionData.x
+    const oldY = laserPositionData.y
+
+    laserPositionData.x = xyz[0]
+    laserPositionData.y = xyz[1]
+    laserPositionData.angle = rpy[2]
+
+    realtimeInfo.coordinates = `(${xyz[0]}, ${xyz[1]}, ${xyz[2] || 0})`
+    realtimeInfo.heading = rpy[2] + '°'
+
+    if (pose.velocity || pose.heading) {
+      realtimeInfo.speed = (pose.heading || 0) + 'm/s'
+      realtimeInfo.speedCommand = (pose.velocity || 0) + 'm/s'
+    }
+
+    if (pose.odometer !== undefined) {
+      realtimeInfo.totalDistance = pose.odometer + 'm'
+    }
+
+    if (pose.confidence !== undefined) {
+      realtimeInfo.registrationError = pose.confidence
+    }
+
+    const positionChanged = Math.abs(xyz[0] - oldX) > 0.005 || Math.abs(xyz[1] - oldY) > 0.005
+
+    if (positionChanged && olmap.value && olmap.value.currentTrajectory) {
+      const currentTrajectory = olmap.value.currentTrajectory
+      if (currentTrajectory && currentTrajectory.length > 0) {
+        const distanceToTarget = calculateDistanceToTrajectory([laserPositionData.x, laserPositionData.y], currentTrajectory)
+        const progress = calculateCurrentProgress(currentTrajectory)
+        if (progress > 0) {
+          progressInfo.progress = progress
+        }
+      }
+    }
+  } catch (error) {
+    console.error('[Navigation] 解析位姿消息失败:', error)
+  }
+}
+
+// WebSocket版本的任务实时信息处理
+const handleTaskRealtimeInfoFromWs = (message) => {
+  try {
+    const data = message.payload || message
+    if (data.battery !== undefined) {
+      realtimeInfo.batteryLevel = (data.battery * 100).toFixed(2) + '%'
+    }
+    if (data.state !== undefined) {
+      realtimeInfo.robotState = data.state
+    }
+  } catch (error) {
+    console.error('[Navigation] 解析任务实时信息失败:', error)
+  }
+}
+
+// WebSocket版本的轨迹数据处理
+const handleTrajectoryDataFromWs = (message) => {
+  handleTrajectoryData(message)
+}
+
+// WebSocket版本的到达事件处理
+const handleArriveEventFromWs = (message) => {
+  handleArriveEvent(message)
+}
+
+// WebSocket版本的导航响应处理
+const handleNavigationResponseFromWs = (message) => {
+  const data = message.payload || message
+  // 处理导航相关的响应
+  if (data.success !== undefined) {
+    if (data.action === 'start') {
+      handleNavigationStartReply(data)
+    } else if (data.action === 'stop') {
+      handleNavigationStopReply(data)
+    }
+  }
+}
+
+// WebSocket版本的规划响应处理
+const handlePlanningResponseFromWs = (message) => {
+  handlePlanResponse(message)
+}
+
+// WebSocket版本的ASM响应处理
+const handleAsmResponseFromWs = (message) => {
+  const data = message.payload || message
+  if (data.result !== undefined) {
+    handleNavigationRestartReply(data)
+  }
+}
+
+// 处理电池信息
+const handleBatteryInfoFromWs = (message) => {
+  try {
+    const data = message.data || message
+    if (data.capacity !== undefined) {
+      // capacity范围0.0~1.0,转换为百分比
+      const capacityPercent = (data.capacity * 100).toFixed(0)
+      realtimeInfo.batteryLevel = capacityPercent + '%'
+
+      // 更新电池详细信息
+      if (realtimeInfo.batteryDetails === undefined) {
+        realtimeInfo.batteryDetails = {}
+      }
+      realtimeInfo.batteryDetails.capacity = data.capacity
+      realtimeInfo.batteryDetails.temperature = data.temperature
+      realtimeInfo.batteryDetails.voltage = data.voltage
+      realtimeInfo.batteryDetails.current = data.current
+      realtimeInfo.batteryDetails.charging = data.charging
+      realtimeInfo.batteryDetails.other = data.other
+
+      console.log(`[Navigation] 电池信息更新: ${capacityPercent}%, 温度: ${data.temperature}°C, 电压: ${data.voltage}V, 充电: ${data.charging}`)
+    }
+  } catch (error) {
+    console.error('[Navigation] 解析电池信息失败:', error)
+  }
+}
+
+// 处理原始数据中的电池信息
+const handleBatteryDataFromRaw = (message) => {
+  try {
+    const data = message.payload || message
+    if (data.capacity !== undefined) {
+      const capacityPercent = (data.capacity * 100).toFixed(0)
+      realtimeInfo.batteryLevel = capacityPercent + '%'
+    }
+  } catch (error) {
+    console.error('[Navigation] 解析原始电池信息失败:', error)
+  }
+}
+
+const handleLaserPose = (message) => {
+  try {
+    const data = message.data || message
+    const { xyz, rpy, blh, heading } = data
+
+    const oldX = laserPositionData.x
+    const oldY = laserPositionData.y
+
+    laserPositionData.x = xyz[0]
+    laserPositionData.y = xyz[1]
+    laserPositionData.angle = rpy[2]
+
+    realtimeInfo.coordinates = `(${xyz[0]}, ${xyz[1]}, ${xyz[2]})`
+    realtimeInfo.heading = rpy[2] + '°'
+    realtimeInfo.speed = data.heading !== null ? data.heading : 0 + 'm/s'
+    realtimeInfo.speedCommand = data.velocity !== null ? data.velocity : 0 + 'm/s'
+    realtimeInfo.totalDistance = data.odometer !== null ? data.odometer : 0 + 'm'
+    realtimeInfo.registrationError = data.confidence
+
+    if (data.runState !== null && data.runState !== undefined) {
+      obstacleStopState.runState = data.runState
+      obstacleStopState.runStateText = getRunStateText(data.runState)
+
+      if (data.runState === 4) {
+        if (obstacleStopState.counter < 25) {
+          obstacleStopState.counter++
+        }
+        if (obstacleStopState.isReplanning) {
+          obstacleStopState.isReplanning = false
+        }
+      } else {
+        if (obstacleStopState.counter > 0) {
+          obstacleStopState.counter--
+        }
+      }
+    }
+
+    if (data.replan !== undefined) {
+      obstacleStopState.replanTimeLeft = data.replan
+    }
+
+    if (obstacleStopState.counter >= 20) {
+      if (!obstacleStopState.showWarning) {
+        obstacleStopState.showWarning = true
+        console.log('检测到稳定遇障状态,显示警告')
+      }
+    } else {
+      if (obstacleStopState.showWarning) {
+        obstacleStopState.showWarning = false
+        console.log('遇障状态解除,隐藏警告')
+      }
+    }
+
+    const positionChanged = Math.abs(xyz[0] - oldX) > 0.005 || Math.abs(xyz[1] - oldY) > 0.005
+
+    if (positionChanged && olmap.value && olmap.value.currentTrajectory) {
+      const currentTrajectory = olmap.value.currentTrajectory
+      const reachedTarget = checkIfReachedTarget(currentTrajectory)
+
+      if (!reachedTarget) {
+        console.log(`机器人位置更新,重新绘制实时轨迹: (${xyz[0].toFixed(3)}, ${xyz[1].toFixed(3)})`)
+        if (olmap.value.updateRealtimeTrajectory) {
+          olmap.value.updateRealtimeTrajectory([xyz[0], xyz[1]])
+        }
+      }
+    }
+  } catch (e) {
+    console.error("解析失败:", e)
+  }
+}
+
+const handlePlanResponse = (message) => {
+  console.log("路径规划响应:", message)
+  if (message.status === 'ok' && message.args && message.args.length > 0) {
+    const planData = message.args[0]
+    console.log("规划路径数据:", planData)
+
+    if (planData.path && planData.path.length > 0) {
+      const plannedPath = planData.path.map(point => {
+        if (planData.div) {
+          return [point[0] / planData.div, point[1] / planData.div]
+        }
+        return [point[0], point[1]]
+      })
+
+      console.log("绘制规划路径,点数:", plannedPath.length)
+
+      if (olmap.value && olmap.value.clearTrajectory) {
+        olmap.value.clearTrajectory()
+        console.log('已清除旧轨迹,准备绘制新的规划路径')
+      }
+
+      if (olmap.value && olmap.value.drawTrajectory) {
+        olmap.value.drawTrajectory(plannedPath, {
+          totalPoints: plannedPath.length,
+          currentProgress: 0,
+          robotPosition: [laserPositionData.x, laserPositionData.y]
+        })
+      }
+
+      ElMessage.success(`路径规划成功!规划路径共 ${plannedPath.length} 个点`)
+    } else {
+      ElMessage.success('路径规划成功!')
+    }
+  } else {
+    ElMessage.error('路径规划失败')
+  }
+}
+
+const handleTrajectoryData = (message) => {
+  console.log("接收到轨迹数据:", message)
+
+  if (!isNavigating.value && !currentNavigationTask.value) {
+    console.log('非导航状态,忽略轨迹数据(可能是retain消息)')
+    return
+  }
+
+  try {
+    if (message.args && message.args.length > 0) {
+      const trajectoryData = message.args[0]
+      const { trj, idx, total, div } = trajectoryData
+
+      const realTrajectory = trj.map(point => [
+        point[0] / div,
+        point[1] / div
+      ])
+
+      const currentProgress = calculateCurrentProgress(realTrajectory)
+
+      if (olmap.value && olmap.value.clearTrajectory) {
+        olmap.value.clearTrajectory()
+        console.log('已清除旧轨迹,准备绘制新的实时轨迹')
+      }
+
+      if (olmap.value && olmap.value.drawTrajectory) {
+        olmap.value.drawTrajectory(realTrajectory, {
+          totalPoints: total,
+          indices: idx,
+          currentProgress: currentProgress,
+          robotPosition: [laserPositionData.x, laserPositionData.y]
+        })
+      }
+
+      console.log("轨迹绘制完成,总点数:", total, "轨迹点数:", realTrajectory.length)
+    }
+  } catch (error) {
+    console.error("处理轨迹数据失败:", error)
+  }
+}
+
+const handleReplanReply = (message) => {
+  console.log("重规划响应:", message)
+  obstacleStopState.isReplanning = false
+
+  if (message.status === 'ok') {
+    ElMessage.success('重规划成功')
+  } else {
+    ElMessage.error('重规划失败')
+  }
+}
+
+const handleGotoReply = (message) => {
+  console.log("前往目标点响应:", message)
+  try {
+    if (message.status === 'ok') {
+      if (lastGotoRequest.value && message.pub_timestamp === lastGotoRequest.value.timestamp) {
+        navigationStatus.value = 'navigating'
+        isNavigating.value = true
+        ElMessage.success('机器人已接收前往指令,开始导航')
+      }
+    } else {
+      navigationStatus.value = 'failed'
+      isNavigating.value = false
+      ElMessage.error('前往目标点请求失败')
+    }
+  } catch (error) {
+    console.error("处理goto响应失败:", error)
+  }
+}
+
+const handleArriveEvent = (message) => {
+  console.log("到达目标点事件:", message)
+  try {
+    if (message.args && message.args.length > 0) {
+      const arriveData = message.args[0]
+      const { status, coord, nid, error } = arriveData
+
+      if (status === 'ok') {
+        navigationStatus.value = 'arrived'
+        isNavigating.value = false
+
+        const targetInfo = coord && coord.length > 0 ?
+          `(${coord[0][0]}, ${coord[0][1]})` :
+          (nid && nid.length > 0 ? `点位${nid[0]}` : '目标点')
+
+        if (currentExecutingTask.value && currentNavigationTask.value?.taskContext) {
+          handleTaskWaypointArrival()
+        } else {
+          ElMessage.success(`已成功到达目标点 ${targetInfo}!`)
+
+          if (olmap.value && olmap.value.clearTrajectory) {
+            setTimeout(() => {
+              olmap.value.clearTrajectory()
+              settingParams.showRoadNetwork = true
+              console.log('轨迹已自动清除')
+            }, 2000)
+          }
+        }
+
+        currentNavigationTask.value = null
+        lastGotoRequest.value = null
+      } else if (status === 'fail') {
+        navigationStatus.value = 'failed'
+        isNavigating.value = false
+
+        let errorMessage = '前往目标点失败'
+        if (error) {
+          switch (error) {
+            case 21: errorMessage = '前往目标点失败:规划失败'; break
+            case 22: errorMessage = '前往目标点失败:偏离车道线'; break
+            case 23: errorMessage = '前往目标点失败:任务提前终止'; break
+            case 24: errorMessage = '前往目标点失败:定位异常'; break
+            default: errorMessage = `前往目标点失败:未知错误(${error})`
+          }
+        }
+
+        ElMessage.error(errorMessage)
+
+        if (olmap.value && olmap.value.clearTrajectory) {
+          olmap.value.clearTrajectory()
+          settingParams.showRoadNetwork = true
+        }
+
+        currentNavigationTask.value = null
+        lastGotoRequest.value = null
+      }
+    }
+  } catch (error) {
+    console.error("处理到达事件失败:", error)
+  }
+}
+
+const handleTaskExecReply = (message) => {
+  console.log("任务执行响应:", message)
+  try {
+    if (message.status === 'ok') {
+      if (lastTaskExecRequest.value && message.pub_timestamp === lastTaskExecRequest.value.timestamp) {
+        taskExecutionStatus.value = 'executing'
+        isTaskExecuting.value = true
+        ElMessage.success(`任务 "${currentExecutingTask.value.taskName}" 已开始执行`)
+        executeNextWaypoint()
+      }
+    } else {
+      taskExecutionStatus.value = 'failed'
+      isTaskExecuting.value = false
+      ElMessage.error('任务执行失败')
+    }
+  } catch (error) {
+    console.error("处理任务执行响应失败:", error)
+  }
+}
+
+const handleTaskCompleteEvent = (message) => {
+  console.log("任务完成事件 - 原始消息:", message)
+  try {
+    if (message.args && message.args.length > 0) {
+      const completeData = message.args[0]
+      let { name, status } = completeData
+
+      const hasEncodingIssue = name && /[\uFFFD\u00C0-\u00FF]{2,}/.test(name)
+      if (hasEncodingIssue) {
+        console.warn("检测到编码问题,任务名称包含乱码:", name)
+      }
+
+      console.log("解析后的任务名称:", name, "状态:", status)
+
+      let currentTask = null
+      let taskNameToDisplay = name
+
+      if (currentExecutingTask.value) {
+        currentTask = currentExecutingTask.value
+        taskNameToDisplay = currentTask.taskName
+        console.log("使用 currentExecutingTask 引用匹配任务:", currentTask.taskName)
+      } else {
+        currentTask = tasks.value.find(t => t.taskName === name)
+        if (!currentTask && hasEncodingIssue) {
+          const runningTasks = tasks.value.filter(t => t.status === 'running' || t.status === 'paused')
+          if (runningTasks.length === 1) {
+            currentTask = runningTasks[0]
+            taskNameToDisplay = currentTask.taskName
+            console.log("通过运行状态匹配到任务:", currentTask.taskName, "(原name为乱码)")
+          }
+        }
+      }
+
+      if (status === 'ok') {
+        taskExecutionStatus.value = 'completed'
+        isTaskExecuting.value = false
+        ElMessage.success(`任务 "${taskNameToDisplay}" 已成功完成!`)
+
+        if (currentTask && scheduledTasksToday.value.includes(currentTask.taskName)) {
+          if (!scheduledTasksExecuted.value.includes(currentTask.taskName)) {
+            scheduledTasksExecuted.value.push(currentTask.taskName)
+            console.log(`定时任务完成: ${currentTask.taskName}`)
+          }
+          currentScheduledTask.value = null
+        }
+
+        currentExecutingTask.value = null
+        currentTaskWaypointIndex.value = 0
+        lastTaskExecRequest.value = null
+
+        if (currentTask) {
+          currentTask.status = 'completed'
+          currentTask.executionStatus = 'completed'
+          currentTask.currentWaypointIndex = 0
+
+          const taskInList = tasks.value.find(t => t.taskName === currentTask.taskName || t.taskId === currentTask.taskId)
+          if (taskInList && taskInList !== currentTask) {
+            taskInList.status = 'completed'
+            taskInList.executionStatus = 'completed'
+            taskInList.currentWaypointIndex = 0
+          }
+
+          const taskNameForReset = currentTask.taskName
+          setTimeout(() => {
+            const taskToReset = tasks.value.find(t => t.taskName === taskNameForReset)
+            if (taskToReset && taskToReset.status === 'completed') {
+              taskToReset.status = 'idle'
+              taskToReset.executionStatus = 'idle'
+            }
+          }, 5000)
+        }
+
+        if (olmap.value && olmap.value.clearTrajectory) {
+          setTimeout(() => {
+            olmap.value.clearTrajectory()
+            settingParams.showRoadNetwork = true
+          }, 3000)
+        }
+      } else if (status === 'fail') {
+        taskExecutionStatus.value = 'failed'
+        isTaskExecuting.value = false
+        ElMessage.error(`任务 "${taskNameToDisplay}" 执行失败`)
+
+        currentExecutingTask.value = null
+        currentTaskWaypointIndex.value = 0
+        lastTaskExecRequest.value = null
+
+        if (currentTask) {
+          currentTask.status = 'error'
+          currentTask.executionStatus = 'failed'
+          currentTask.currentWaypointIndex = 0
+
+          const taskInList = tasks.value.find(t => t.taskName === currentTask.taskName || t.taskId === currentTask.taskId)
+          if (taskInList && taskInList !== currentTask) {
+            taskInList.status = 'error'
+            taskInList.executionStatus = 'failed'
+            taskInList.currentWaypointIndex = 0
+          }
+
+          const taskNameForReset = currentTask.taskName
+          setTimeout(() => {
+            const taskToReset = tasks.value.find(t => t.taskName === taskNameForReset)
+            if (taskToReset && taskToReset.status === 'error') {
+              taskToReset.status = 'idle'
+              taskToReset.executionStatus = 'idle'
+            }
+          }, 5000)
+        }
+
+        if (olmap.value && olmap.value.clearTrajectory) {
+          olmap.value.clearTrajectory()
+          settingParams.showRoadNetwork = true
+        }
+      }
+    }
+  } catch (error) {
+    console.error("处理任务完成事件失败:", error)
+  }
+}
+
+const handleTaskRealtimeInfo = (message) => {
+  try {
+    if (message.args && message.args.length > 0) {
+      const realtimeData = message.args[0]
+      const { odom, time, drive_mode, auto_ready } = realtimeData
+
+      taskRealtimeInfo.odom.total = odom?.tottal || 0
+      taskRealtimeInfo.odom.remain = odom?.remain || 0
+      taskRealtimeInfo.time.total = time?.total || 0
+      taskRealtimeInfo.time.remain = time?.remain || 0
+      taskRealtimeInfo.time.duration = time?.duration || 0
+      taskRealtimeInfo.driveMode = drive_mode || 'auto'
+      taskRealtimeInfo.autoReady = auto_ready !== undefined ? auto_ready : true
+
+      if (currentExecutingTask.value) {
+        realtimeInfo.currentTask = currentExecutingTask.value.taskName
+        realtimeInfo.totalDistance = `${(odom?.tottal / 1000).toFixed(2)}km` || ''
+      }
+    }
+  } catch (error) {
+    console.error("处理任务实时信息失败:", error)
+  }
+}
+
+const handleTaskStartReply = (message) => {
+  console.log("任务开始响应:", message)
+  try {
+    if (message.status === 'ok') {
+      ElMessage.success('任务已成功开始执行')
+
+      if (currentExecutingTask.value) {
+        const taskName = currentExecutingTask.value.taskName
+        currentExecutingTask.value.status = 'running'
+        currentExecutingTask.value.executionStatus = 'executing'
+
+        const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
+        if (taskInList) {
+          taskInList.status = 'running'
+          taskInList.executionStatus = 'executing'
+        }
+
+        if (scheduledTasksToday.value.includes(taskName)) {
+          currentScheduledTask.value = {
+            name: taskName,
+            startTime: new Date().getTime(),
+            status: 'running'
+          }
+          lastTaskStartTime.value[taskName] = new Date().getTime()
+          updateTaskListScheduleStatus()
+        }
+      }
+    } else {
+      taskExecutionStatus.value = 'failed'
+      isTaskExecuting.value = false
+
+      if (currentExecutingTask.value) {
+        currentExecutingTask.value.status = 'idle'
+        currentExecutingTask.value.executionStatus = 'failed'
+
+        const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
+        if (taskInList) {
+          taskInList.status = 'idle'
+          taskInList.executionStatus = 'failed'
+        }
+      }
+
+      ElMessage.error('任务开始失败')
+    }
+  } catch (error) {
+    console.error("处理任务开始响应失败:", error)
+  }
+}
+
+const handleTaskPauseReply = (message) => {
+  console.log("暂停任务响应:", message)
+  if (message.status === 'ok') {
+    taskExecutionStatus.value = 'paused'
+
+    if (currentExecutingTask.value) {
+      currentExecutingTask.value.status = 'paused'
+      currentExecutingTask.value.executionStatus = 'paused'
+
+      const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
+      if (taskInList) {
+        taskInList.status = 'paused'
+        taskInList.executionStatus = 'paused'
+      }
+    }
+
+    ElMessage.success('任务已暂停')
+  } else {
+    ElMessage.error('任务暂停失败')
+  }
+}
+
+const handleTaskResumeReply = (message) => {
+  console.log("继续任务响应:", message)
+  if (message.status === 'ok') {
+    taskExecutionStatus.value = 'executing'
+
+    if (currentExecutingTask.value) {
+      currentExecutingTask.value.status = 'running'
+      currentExecutingTask.value.executionStatus = 'executing'
+
+      const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
+      if (taskInList) {
+        taskInList.status = 'running'
+        taskInList.executionStatus = 'executing'
+      }
+    }
+
+    ElMessage.success('任务已继续执行')
+  } else {
+    ElMessage.error('任务继续失败')
+  }
+}
+
+const handleTaskCancelReply = (message) => {
+  console.log("取消任务响应:", message)
+  if (message.status === 'ok') {
+    taskExecutionStatus.value = 'cancelled'
+    isTaskExecuting.value = false
+
+    const cancelledTaskName = currentExecutingTask.value?.taskName
+
+    currentExecutingTask.value = null
+    currentTaskWaypointIndex.value = 0
+    lastTaskExecRequest.value = null
+
+    if (cancelledTaskName) {
+      const taskInList = tasks.value.find(t => t.taskName === cancelledTaskName)
+      if (taskInList) {
+        taskInList.status = 'idle'
+        taskInList.executionStatus = 'idle'
+        taskInList.currentWaypointIndex = 0
+      }
+    }
+
+    ElMessage.success('任务已取消')
+
+    if (olmap.value && olmap.value.clearTrajectory) {
+      olmap.value.clearTrajectory()
+      settingParams.showRoadNetwork = true
+    }
+  } else {
+    ElMessage.error('任务取消失败')
+  }
+}
+
+const handleNavigationStartReply = (message) => {
+  console.log("导航启动响应:", message)
+  try {
+    if (message.status === 'ok') {
+      navigationStackStatus.value = 'started'
+      ElMessage.success('导航系统已启动')
+    } else {
+      ElMessage.error('导航系统启动失败')
+    }
+  } catch (error) {
+    console.error("处理导航启动响应失败:", error)
+  }
+}
+
+const handleNavigationStopReply = (message) => {
+  console.log("导航停止响应:", message)
+  try {
+    if (message.status === 'ok') {
+      navigationStackStatus.value = 'stopped'
+      isNavigating.value = false
+      navigationStatus.value = 'idle'
+      currentNavigationTask.value = null
+      lastGotoRequest.value = null
+
+      if (olmap.value && olmap.value.clearTrajectory) {
+        olmap.value.clearTrajectory()
+        settingParams.showRoadNetwork = true
+      }
+
+      ElMessage.success('导航系统已停止')
+    } else {
+      ElMessage.error('导航系统停止失败')
+    }
+  } catch (error) {
+    console.error("处理导航停止响应失败:", error)
+  }
+}
+
+const handleNavigationRestartReply = (message) => {
+  console.log("导航重启响应:", message)
+  try {
+    if (message.status === 'ok') {
+      navigationStackStatus.value = 'started'
+      isNavigating.value = false
+      navigationStatus.value = 'idle'
+      currentNavigationTask.value = null
+      lastGotoRequest.value = null
+      isTaskExecuting.value = false
+      taskExecutionStatus.value = 'idle'
+      currentExecutingTask.value = null
+      currentTaskWaypointIndex.value = 0
+
+      ElMessage.success('导航系统已重启')
+      isBusy.value = false
+    } else {
+      ElMessage.error('导航系统重启失败')
+      isBusy.value = false
+    }
+  } catch (error) {
+    console.error("处理导航重启响应失败:", error)
+    isBusy.value = false
+  }
+}
+
+const handleEmergencyStopReply = (message) => {
+  console.log("急停控制响应:", message)
+  try {
+    if (message.status === 'ok') {
+      ElMessage.success('急停指令已执行')
+    } else {
+      ElMessage.error('急停指令执行失败')
+    }
+  } catch (error) {
+    console.error("处理急停控制响应失败:", error)
+  }
+}
+
+const handleEmergencyStopStatus = (message) => {
+  try {
+    if (message.args && message.args.length > 0) {
+      const stopEnabled = message.args[0]
+      const oldStatus = emergencyStopEnabled.value
+      emergencyStopEnabled.value = stopEnabled
+
+      if (oldStatus !== stopEnabled) {
+        if (stopEnabled) {
+          ElMessage.warning('车辆已启用急停状态')
+          isNavigating.value = false
+          navigationStatus.value = 'idle'
+          currentNavigationTask.value = null
+          isTaskExecuting.value = false
+          taskExecutionStatus.value = 'idle'
+        } else {
+          ElMessage.info('车辆急停状态已解除')
+        }
+      }
+    }
+  } catch (error) {
+    console.error("处理急停状态失败:", error)
+  }
+}
+
+const handleObstacleData = (message) => {
+  try {
+    if (message.args && message.args.length > 0) {
+      const obstacleInfo = message.args[0]
+      const obsDistance = obstacleInfo.obs_distanse || obstacleInfo.obs_distance
+
+      if (obsDistance && Array.isArray(obsDistance) && obsDistance.length >= 3) {
+        obstacleData.left = obsDistance[0][0] || 0
+        obstacleData.front = obsDistance[1][0] || 0
+        obstacleData.right = obsDistance[2][0] || 0
+        obstacleData.timestamp = message.timestamp || Date.now()
+
+        checkObstacleWarning()
+
+        console.log("障碍物数据更新:", {
+          left: obstacleData.left + 'm',
+          front: obstacleData.front + 'm',
+          right: obstacleData.right + 'm'
+        })
+      }
+    }
+  } catch (error) {
+    console.error("处理障碍物数据失败:", error)
+  }
+}
+
+const checkObstacleWarning = () => {
+  if (!obstacleSettings.enableWarning) return
+
+  const { left, front, right } = obstacleData
+  const { warningDistance, dangerDistance } = obstacleSettings
+
+  let minDistance = Infinity
+  let minDirection = ''
+
+  if (left > 0 && left < minDistance) {
+    minDistance = left
+    minDirection = '左侧'
+  }
+  if (front > 0 && front < minDistance) {
+    minDistance = front
+    minDirection = '前方'
+  }
+  if (right > 0 && right < minDistance) {
+    minDistance = right
+    minDirection = '右侧'
+  }
+
+  if (minDistance !== Infinity) {
+    let level = 'info'
+    let msg = ''
+
+    if (minDistance <= dangerDistance) {
+      level = 'danger'
+      msg = `危险!${minDirection}发现障碍物,距离仅${minDistance.toFixed(1)}米`
+    } else if (minDistance <= warningDistance) {
+      level = 'warning'
+      msg = `注意!${minDirection}发现障碍物,距离${minDistance.toFixed(1)}米`
+    } else {
+      hideObstacleWarning()
+      return
+    }
+
+    showObstacleWarning(level, msg, minDirection, minDistance)
+  } else {
+    hideObstacleWarning()
+  }
+}
+
+const showObstacleWarning = (level, message, direction, distance) => {
+  if (obstacleWarning.autoHideTimer) {
+    clearTimeout(obstacleWarning.autoHideTimer)
+  }
+
+  obstacleWarning.show = true
+  obstacleWarning.level = level
+  obstacleWarning.message = message
+  obstacleWarning.direction = direction
+  obstacleWarning.distance = distance
+  obstacleWarning.autoHideTimer = null
+
+  if (level !== 'danger') {
+    obstacleWarning.autoHideTimer = setTimeout(() => {
+      hideObstacleWarning()
+    }, obstacleSettings.autoHideDelay)
+  }
+}
+
+const hideObstacleWarning = () => {
+  if (obstacleWarning.autoHideTimer) {
+    clearTimeout(obstacleWarning.autoHideTimer)
+  }
+
+  obstacleWarning.show = false
+  obstacleWarning.level = 'info'
+  obstacleWarning.message = ''
+  obstacleWarning.direction = ''
+  obstacleWarning.distance = 0
+  obstacleWarning.autoHideTimer = null
+}
+
+const closeObstacleWarning = () => {
+  hideObstacleWarning()
+}
+
+const getRunStateText = (runState) => {
+  const stateMap = {
+    1: '正常行驶',
+    2: '暂停状态',
+    3: '停车待命',
+    4: '遇障停车',
+    5: '交通管制等待',
+    6: '定位切换等待',
+    7: '人工接管',
+    8: '绕障状态'
+  }
+  return stateMap[runState] || '未知状态'
+}
+
+const handleReplanRequest = async () => {
+  if (obstacleStopState.isReplanning) {
+    ElMessage.warning('正在请求重规划,请稍候...')
+    return
+  }
+
+  try {
+    await replan()
+    obstacleStopState.isReplanning = true
+    ElMessage.success('重规划指令已发送')
+  } catch (error) {
+    console.error('重规划请求失败:', error)
+    ElMessage.error('重规划请求失败')
+  }
+}
+
+const calculateTodayScheduledTasks = () => {
+  if (!tasks.value || tasks.value.length === 0) return
+
+  const scheduledTasksList = tasks.value.filter(task => {
+    return task.shouldRunToday && task.scheduledTime !== null
+  })
+
+  scheduledTasksList.sort((a, b) => {
+    if (!a.scheduledTime || !b.scheduledTime) return 0
+    return a.scheduledTime.getTime() - b.scheduledTime.getTime()
+  })
+
+  scheduledTasksToday.value = scheduledTasksList.map(task => task.taskName)
+
+  console.log(`今日应执行的定时任务 (${scheduledTasksToday.value.length}):`,
+    scheduledTasksList.map(t => `${t.taskName} (${t.scheduledTimeStr})`))
+
+  scheduledTasksList.forEach(task => {
+    console.log(`⏰ ${task.taskName}:`, {
+      time: task.scheduledTimeStr,
+      weekDays: task.scheduledWeekDays,
+      points: task.points.length,
+      count: task.count
+    })
+  })
+
+  updateTaskListScheduleStatus()
+}
+
+const updateTaskListScheduleStatus = () => {
+  if (!tasks.value || tasks.value.length === 0) return
+
+  tasks.value.forEach(task => {
+    task.scheduleStatus = null
+    task.scheduleProgress = null
+
+    if (!scheduledTasksToday.value.includes(task.taskName)) {
+      return
+    }
+
+    if (currentScheduledTask.value && task.taskName === currentScheduledTask.value.name) {
+      task.scheduleStatus = 'schedule-running'
+      if (currentExecutingTask.value && currentExecutingTask.value.taskName === task.taskName) {
+        task.scheduleProgress = `${currentTaskWaypointIndex.value}/${currentExecutingTask.value.points?.length || 0}`
+      }
+    } else if (scheduledTasksExecuted.value.includes(task.taskName)) {
+      task.scheduleStatus = 'schedule-executed'
+    } else {
+      task.scheduleStatus = 'schedule-pending'
+    }
+  })
+}
+
+const startAutoExecuteTimer = () => {
+  if (!autoExecuteEnabled.value) {
+    console.log('定时任务自动执行功能已禁用')
+    return
+  }
+
+  stopAutoExecuteTimer()
+
+  autoExecuteTimer.value = setInterval(() => {
+    checkAndExecuteScheduledTasks()
+  }, 30 * 1000)
+
+  console.log('定时任务自动执行功能已启动(每30秒检查一次)')
+  checkAndExecuteScheduledTasks()
+}
+
+const stopAutoExecuteTimer = () => {
+  if (autoExecuteTimer.value) {
+    clearInterval(autoExecuteTimer.value)
+    autoExecuteTimer.value = null
+    console.log('定时任务自动执行功能已停止')
+  }
+}
+
+const checkAndExecuteScheduledTasks = () => {
+  if (!autoExecuteEnabled.value || !tasks.value || tasks.value.length === 0) {
+    return
+  }
+
+  if (isTaskExecuting.value || isNavigating.value) {
+    return
+  }
+
+  const now = new Date()
+  const currentTime = now.getTime()
+
+  const tasksToExecute = tasks.value.filter(task => {
+    if (!task.shouldRunToday || !task.scheduledTime) return false
+    if (scheduledTasksExecuted.value.includes(task.taskName)) return false
+    if (currentScheduledTask.value && currentScheduledTask.value.name === task.taskName) return false
+
+    const scheduledTimeMs = task.scheduledTime.getTime()
+    const timeDiff = currentTime - scheduledTimeMs
+
+    return timeDiff >= 0 && timeDiff < 5 * 60 * 1000
+  })
+
+  tasksToExecute.sort((a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime())
+
+  if (tasksToExecute.length > 0) {
+    const taskToExecute = tasksToExecute[0]
+    console.log(`自动执行定时任务: ${taskToExecute.taskName} (预定时间: ${taskToExecute.scheduledTimeStr})`)
+    executeScheduledTask(taskToExecute)
+  }
+}
+
+const executeScheduledTask = async (task) => {
+  if (!task || !task.points || task.points.length === 0) {
+    console.error('任务数据不完整,无法执行:', task.taskName)
+    return
+  }
+
+  console.log(`开始执行定时任务: ${task.taskName}`)
+  console.log(`包含 ${task.points.length} 个目标点`)
+  console.log(`执行次数: ${task.count}`)
+
+  try {
+    const timestamp = new Date().getTime()
+
+    currentExecutingTask.value = task
+    currentTaskWaypointIndex.value = 0
+    lastTaskExecRequest.value = { roadmap: mapName.value, task: task.taskName }
+    isTaskExecuting.value = true
+    taskExecutionStatus.value = 'executing'
+
+    task.status = 'running'
+    task.executionStatus = 'executing'
+
+    // 发送启动任务请求
+    await startTask(mapName.value, task.taskName)
+
+    ElMessage.success(`自动执行定时任务: ${task.taskName}`)
+
+    console.log(`定时任务 "${task.taskName}" 执行指令已发送`)
+  } catch (error) {
+    console.error(`执行定时任务 "${task.taskName}" 失败:`, error)
+    ElMessage.error(`执行定时任务失败: ${error.message}`)
+  }
+}
+
+const calculateCurrentProgress = (trajectory) => {
+  if (!trajectory || trajectory.length === 0) return 0
+
+  const robotPos = [robotPoseData.x, robotPoseData.y]
+  let totalDistance = 0
+
+  for (let i = 0; i < trajectory.length - 1; i++) {
+    const segmentLength = Math.sqrt(
+      Math.pow(trajectory[i + 1][0] - trajectory[i][0], 2) +
+      Math.pow(trajectory[i + 1][1] - trajectory[i][1], 2)
+    )
+    totalDistance += segmentLength
+  }
+
+  if (totalDistance === 0) return 0
+
+  let bestProjectionDistance = 0
+  let minDistanceToTrajectory = Infinity
+
+  let accumulatedDistance = 0
+  for (let i = 0; i < trajectory.length - 1; i++) {
+    const segmentStart = trajectory[i]
+    const segmentEnd = trajectory[i + 1]
+    const segmentLength = Math.sqrt(
+      Math.pow(segmentEnd[0] - segmentStart[0], 2) +
+      Math.pow(segmentEnd[1] - segmentStart[1], 2)
+    )
+
+    const projection = projectPointToLineSegment(robotPos, segmentStart, segmentEnd)
+    const distanceToSegment = projection.distance
+
+    if (distanceToSegment < minDistanceToTrajectory) {
+      minDistanceToTrajectory = distanceToSegment
+      const projectionDistanceOnSegment = Math.sqrt(
+        Math.pow(projection.point[0] - segmentStart[0], 2) +
+        Math.pow(projection.point[1] - segmentStart[1], 2)
+      )
+      bestProjectionDistance = accumulatedDistance + projectionDistanceOnSegment
+    }
+
+    accumulatedDistance += segmentLength
+  }
+
+  const progressRatio = bestProjectionDistance / totalDistance
+  const progressIndex = Math.floor(progressRatio * (trajectory.length - 1))
+
+  if (minDistanceToTrajectory < 5.0) {
+    console.log(`机器人轨迹进度: ${progressIndex}/${trajectory.length-1}, 距轨迹距离: ${minDistanceToTrajectory.toFixed(2)}m`)
+    return Math.min(progressIndex, trajectory.length - 1)
+  }
+
+  return olmap.value?.trajectoryProgress || 0
+}
+
+const projectPointToLineSegment = (point, lineStart, lineEnd) => {
+  const [px, py] = point
+  const [x1, y1] = lineStart
+  const [x2, y2] = lineEnd
+
+  const A = px - x1
+  const B = py - y1
+  const C = x2 - x1
+  const D = y2 - y1
+
+  const dot = A * C + B * D
+  const lenSq = C * C + D * D
+
+  if (lenSq === 0) {
+    return {
+      point: [x1, y1],
+      distance: Math.sqrt(A * A + B * B)
+    }
+  }
+
+  let param = dot / lenSq
+  param = Math.max(0, Math.min(1, param))
+
+  const projectionX = x1 + param * C
+  const projectionY = y1 + param * D
+
+  const distance = Math.sqrt(
+    Math.pow(px - projectionX, 2) + Math.pow(py - projectionY, 2)
+  )
+
+  return {
+    point: [projectionX, projectionY],
+    distance: distance
+  }
+}
+
+const checkIfReachedTarget = (trajectory) => {
+  if (!trajectory || trajectory.length === 0) return false
+
+  if (navigationStatus.value === 'arrived' || navigationStatus.value === 'failed') {
+    return true
+  }
+
+  const robotPos = [laserPositionData.x, laserPositionData.y]
+  const targetPos = trajectory[trajectory.length - 1]
+
+  const distanceToTarget = Math.sqrt(
+    Math.pow(targetPos[0] - robotPos[0], 2) + Math.pow(targetPos[1] - robotPos[1], 2)
+  )
+
+  if (distanceToTarget < 1.0 && isNavigating.value) {
+    console.log(`本地检测: 机器人接近目标点,距离: ${distanceToTarget.toFixed(2)}m,等待WebSocket到达确认`)
+  }
+
+  return false
+}
+
+const publishMsg = () => {}
+
+const updateOlCss = () => {
+  const element = mapStage.value
+  if (element) {
+    const newWidth = element.offsetWidth
+    const newHeight = element.offsetHeight
+    // 调试日志:检测容器尺寸变化
+    if (newWidth === 0 || newHeight === 0) {
+      console.warn('[updateOlCss] 容器尺寸为0,可能需要等待DOM渲染完成', { width: newWidth, height: newHeight })
+    }
+    olWidth.value = newWidth
+    olHeight.value = newHeight
+  } else {
+    console.warn('[updateOlCss] mapStage element 为空')
+  }
+}
+
+const getMapInstance = () => {
+  return olmap.value && olmap.value.map ? olmap.value.map : null
+}
+
+const openDraSetting = () => {
+  initPoseMode.value = false
+  settingDrawer.value = !settingDrawer.value
+  if (settingDrawer.value) {
+    closeDra()
+    nowHandMenu.value = '功能菜单操作'
+  } else {
+    nowHandMenu.value = ''
+  }
+}
+
+const openPoint = () => {
+  initPoseMode.value = false
+  pointDrawer.value = !pointDrawer.value
+  if (pointDrawer.value) {
+    closeDra()
+    nowHandMenu.value = '目标点操作'
+  } else {
+    nowHandMenu.value = ''
+  }
+}
+
+const openTask = () => {
+  initPoseMode.value = false
+  taskDrawer.value = !taskDrawer.value
+  if (taskDrawer.value) {
+    closeDra()
+    nowHandMenu.value = '任务操作'
+  } else {
+    nowHandMenu.value = ''
+  }
+}
+
+const closeDra = (type) => {
+  settingDrawer.value = false
+  pointDrawer.value = false
+  taskDrawer.value = false
+  if (type == 'hand') {
+    activeTab.value = -1
+  }
+  nowHandMenu.value = ''
+}
+
+const handleSelectionChange = (selection) => {
+  pointIds.value = selection.map(item => item.id)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+const editPoint = (row) => {
+  pointEditDiaShow.value = true
+  pointEditData.id = row.id
+  pointEditData.x = row.x
+  pointEditData.y = row.y
+  pointEditData.type = row.type
+  pointEditData.actionMenuList = row.action
+}
+
+const removePoint = (row) => {
+  const idArr = []
+  const ids = row?.id || pointIds.value
+  if (ids) {
+    idArr.push(...(Array.isArray(ids) ? ids : [ids]))
+    idArr.forEach(id => {
+      let indexToRemove = pointList.value.findIndex(item => item?.id === id)
+      if (indexToRemove !== -1) {
+        pointList.value.splice(indexToRemove, 1)
+        if (pointList.value.length < 1) {
+          olmap.value?.restIdNum()
+        }
+        olmap.value?.removeCalibrationById(id)
+      }
+    })
+  }
+}
+
+const moveUp = () => {
+  const index = pointList.value.findIndex(point => point.id == pointIds.value[0])
+  if (index !== -1 && index > 0) {
+    const temp = pointList.value[index]
+    pointList.value[index] = pointList.value[index - 1]
+    pointList.value[index - 1] = temp
+  }
+}
+
+const moveDown = () => {
+  const index = pointList.value.findIndex(point => point.id == pointIds.value[0])
+  if (index !== -1 && index < pointList.value.length - 1) {
+    const temp = pointList.value[index]
+    pointList.value[index] = pointList.value[index + 1]
+    pointList.value[index + 1] = temp
+  }
+}
+
+const appendActionMenu = () => {
+  pointEditData.actionMenuList.push({ value: 0, other: 0 })
+}
+
+const removeActionMenu = (index) => {
+  pointEditData.actionMenuList.splice(index, 1)
+}
+
+const changeAction = (index) => {
+  const item = pointEditData.actionMenuList[index]
+  if (item && 'other' in item) {
+    delete item.other
+  } else {
+    item.other = 0
+  }
+}
+
+const clearActionDia = () => {
+  pointEditData.id = ''
+  pointEditData.x = ''
+  pointEditData.y = ''
+  pointEditData.type = ''
+  pointEditData.actionMenuList = []
+}
+
+const submitEditPoint = () => {
+  pointEditDiaShow.value = false
+  const point = waypoints.value.find(item => item.id === pointEditData.id)
+  if (point) {
+    point.x = pointEditData.x
+    point.y = pointEditData.y
+    point.type = pointEditData.type
+    point.action = [...pointEditData.actionMenuList]
+    ElMessage.success("目标点数据已修改")
+  }
+}
+
+const submitTaskGenerate = async () => {
+  const orderedPoints = generateTaskParam.selectedWaypoints || []
+  if (!generateTaskParam.taskName || orderedPoints.length === 0) {
+    ElMessage.warning('请完善任务数据(任务名称和目标点不能为空)!')
+    return
+  }
+
+  try {
+    const coordinates = orderedPoints.map(point => [
+      parseFloat(point.x),
+      parseFloat(point.y),
+      0
+    ])
+
+    const plan = orderedPoints.map(point => {
+      return point.type === 1 ? 'route' : 'free'
+    })
+
+    const action = orderedPoints.map(point => {
+      if (!point.action || point.action.length === 0) {
+        return []
+      }
+      return point.action.map(act => {
+        if (act.value === 0 && 'other' in act) {
+          return { type: 'wait', duration: parseFloat(act.other) || 0 }
+        } else {
+          const actionTypes = ['wait', 'start_record', 'stop_record', 'add_trajectory', 'hook_mount', 'hook_unmount']
+          return { type: actionTypes[act.value] || 'wait' }
+        }
+      })
+    })
+
+    let cron = ''
+    if (generateTaskParam.time && generateTaskParam.date.length > 0) {
+      let hours, minutes
+      if (typeof generateTaskParam.time === 'string') {
+        const timeParts = generateTaskParam.time.split(':')
+        hours = timeParts[0]
+        minutes = timeParts[1]
+      } else {
+        const timeObj = new Date(generateTaskParam.time)
+        hours = String(timeObj.getHours()).padStart(2, '0')
+        minutes = String(timeObj.getMinutes()).padStart(2, '0')
+      }
+      const weekdays = generateTaskParam.date.sort().join(',')
+      cron = `00 ${minutes} ${hours} ? * ${weekdays} *`
+    }
+
+    const taskData = {
+      map: mapName.value,
+      path: generateTaskParam.taskName,
+      coord_type: 'local',
+      cron: cron,
+      coord: coordinates,
+      plan: plan,
+      action: [[]],
+      repeate: generateTaskParam.count || 1
+    }
+
+    const response = await addTask(taskData)
+    console.log("创建任务响应:", response)
+
+    if (response.status === true) {
+      ElMessage.success(`任务创建成功!任务包含 ${orderedPoints.length} 个目标点`)
+      await loadTaskList()
+      restGenerateParam()
+      taskGenerateDiaShow.value = false
+      selectedWaypointIds.value = []
+      console.log("任务创建成功:", taskData)
+    } else {
+      ElMessage.error('任务创建失败: ' + (response.msg || '未知错误'))
+    }
+  } catch (error) {
+    console.error("创建任务失败:", error)
+    ElMessage.error('任务创建失败: ' + (error.message || '未知错误'))
+  }
+}
+
+const initNavigation = () => {
+  closeDra()
+  initPoseMode.value = !initPoseMode.value
+  nowHandMenu.value = initPoseMode.value ? '初始化导航' : ''
+  selectPointMode.value = false
+}
+
+const restNavigation = async () => {
+  try {
+    await ElMessageBox.confirm('将重启当前导航, 是否继续?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('导航已重启!')
+  } catch {
+    // 用户取消
+  }
+}
+
+const offNavigation = async () => {
+  try {
+    await ElMessageBox.confirm('将关闭当前导航, 是否继续?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('导航已关闭!')
+  } catch {
+    // 用户取消
+  }
+}
+
+const closeTaskGenerate = () => {
+  taskGenerateDiaShow.value = false
+  restGenerateParam()
+}
+
+const restGenerateParam = () => {
+  generateTaskParam.taskId = ''
+  generateTaskParam.taskName = ''
+  generateTaskParam.count = 1
+  generateTaskParam.time = ''
+  generateTaskParam.date = []
+  generateTaskParam.selectedWaypoints = []
+}
+
+const removeTaskItem = async (data) => {
+  try {
+    await ElMessageBox.confirm('删除名为' + data.taskName + '的任务', '删除', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('已删除!')
+    taskDataList.value = taskDataList.value.filter(task => task.taskId !== data.taskId)
+  } catch {
+    // 用户取消
+  }
+}
+
+const executeTask = async (data) => {
+  try {
+    await ElMessageBox.confirm('开始执行任务' + data.taskName + '?', '执行', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('任务已开始执行!')
+    taskDataList.value.forEach(task => {
+      if (task.taskId === data.taskId) {
+        task.status = 0
+      }
+    })
+  } catch {
+    // 用户取消
+  }
+}
+
+const test = () => {
+  console.log(settingParams)
+}
+
+const mapSelectEle = (type) => {
+  if (type == 'open') {
+    selectPointMode.value = true
+    initPoseMode.value = false
+    ElMessage.success('已开启选择模式')
+  } else {
+    selectPointMode.value = false
+    ElMessage.info('已关闭选择模式')
+  }
+}
+
+const addNowPoint = (currentCoordinate, currentPlace) => {
+  const coordData = {
+    id: currentCoordinate[0],
+    name: `目标点${currentCoordinate[0]}`,
+    x: currentCoordinate[1].toFixed(3),
+    y: currentCoordinate[2].toFixed(3),
+    placeX: currentPlace[0].toFixed(3),
+    placeY: currentPlace[1].toFixed(3),
+    type: 1,
+    action: [{ value: 0, other: 0 }]
+  }
+  waypoints.value.push(coordData)
+  ElMessage.success(`已添加目标点: (${coordData.x}, ${coordData.y})`)
+}
+
+const initNavigationResult = (position, yaw, nid) => {
+  console.log("=== 位姿初始化 ===")
+  console.log("位置:", `x=${position[0].toFixed(3)}, y=${position[1].toFixed(3)}`)
+  console.log("朝向角(弧度):", yaw.toFixed(4))
+  console.log("朝向角(度):", (yaw * 180 / Math.PI).toFixed(2) + "°")
+  console.log("点位ID:", nid || "null")
+
+  // 根据是否有nid选择使用哪个接口
+  if (nid) {
+    initPoseByNid(nid).then(() => {
+      ElMessage.success({
+        message: `位姿初始化成功!位置: (${position[0].toFixed(2)}, ${position[1].toFixed(2)}), 朝向: ${(yaw * 180 / Math.PI).toFixed(1)}°`,
+        duration: 3000
+      })
+    }).catch(error => {
+      console.error('位姿初始化失败:', error)
+      ElMessage.error('位姿初始化失败')
+    })
+  } else {
+    initPose(position[0], position[1], yaw).then(() => {
+      ElMessage.success({
+        message: `位姿初始化成功!位置: (${position[0].toFixed(2)}, ${position[1].toFixed(2)}), 朝向: ${(yaw * 180 / Math.PI).toFixed(1)}°`,
+        duration: 3000
+      })
+    }).catch(error => {
+      console.error('位姿初始化失败:', error)
+      ElMessage.error('位姿初始化失败')
+    })
+  }
+
+  initPoseMode.value = false
+  nowHandMenu.value = ''
+}
+
+const onTabChange = (tabKey) => {
+  activeTab.value = tabKey
+  lastTab.value = tabKey
+
+  if (tabKey === 'points' && selectPointMode.value) {
+    // 保持选点模式
+  } else if (tabKey !== 'points') {
+    selectPointMode.value = false
+    nowHandMenu.value = ''
+  }
+}
+
+const toggleSelectPointMode = () => {
+  selectPointMode.value = !selectPointMode.value
+  initPoseMode.value = false
+  nowHandMenu.value = selectPointMode.value ? '选点模式' : ''
+
+  if (selectPointMode.value) {
+    ElMessage.success('已进入选点模式')
+  } else {
+    ElMessage.info('已退出选点模式')
+  }
+}
+
+const clearAllPoints = async () => {
+  try {
+    await ElMessageBox.confirm('确定要清空所有点位吗?', '确认清空', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    waypoints.value = []
+    if (olmap.value && olmap.value.restIdNum) {
+      olmap.value.restIdNum()
+    }
+    ElMessage.success('已清空所有点位')
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleZoomIn = () => {
+  try {
+    const map = getMapInstance()
+    if (map) {
+      const view = map.getView()
+      const currentZoom = view.getZoom()
+      view.animate({
+        zoom: currentZoom + 1,
+        duration: 250
+      })
+    } else if (olmap.value && olmap.value.zoomIn) {
+      olmap.value.zoomIn()
+    }
+  } catch (error) {
+    console.warn('地图放大失败:', error)
+    ElMessage.warning('地图放大失败')
+  }
+}
+
+const handleZoomOut = () => {
+  try {
+    const map = getMapInstance()
+    if (map) {
+      const view = map.getView()
+      const currentZoom = view.getZoom()
+      view.animate({
+        zoom: Math.max(currentZoom - 1, 1),
+        duration: 250
+      })
+    } else if (olmap.value && olmap.value.zoomOut) {
+      olmap.value.zoomOut()
+    }
+  } catch (error) {
+    console.warn('地图缩小失败:', error)
+    ElMessage.warning('地图缩小失败')
+  }
+}
+
+const handleCenterToRobot = () => {
+  try {
+    const map = getMapInstance()
+    if (map) {
+      const view = map.getView()
+
+      let centerPoint = [robotPoseData.x, robotPoseData.y]
+      if (robotPoseData.x === 0 && robotPoseData.y === 0) {
+        centerPoint = view.getCenter()
+        console.log('使用地图中心点作为居中位置:', centerPoint)
+      } else {
+        console.log('使用机器人位置作为居中位置:', centerPoint)
+      }
+
+      view.animate({
+        center: centerPoint,
+        zoom: Math.max(view.getZoom(), 15),
+        duration: 500
+      })
+      ElMessage.success('已居中到机器人位置')
+    } else if (olmap.value && olmap.value.centerToRobot) {
+      olmap.value.centerToRobot()
+      ElMessage.success('已居中到机器人位置')
+    } else {
+      console.warn('无法获取地图实例')
+      ElMessage.warning('地图未就绪,无法居中')
+    }
+  } catch (error) {
+    console.error('定位机器人失败:', error)
+    console.log('调试信息:', {
+      robotPoseData: robotPoseData,
+      robotPosition: robotPosition.value,
+      mapReady: !!getMapInstance()
+    })
+    ElMessage.warning('定位机器人失败: ' + error.message)
+  }
+}
+
+const handleToggleFullscreen = () => {
+  const mapContainer = mapStage.value
+  if (!mapContainer) {
+    ElMessage.error('无法找到地图容器')
+    return
+  }
+
+  if (FullscreenOperations.toggleFullscreen(mapContainer)) {
+    // 全屏切换成功
+  } else {
+    ElMessage.error('浏览器不支持全屏功能')
+  }
+}
+
+const handleConfirmInit = () => {
+  initPoseMode.value = true
+  selectPointMode.value = false
+  nowHandMenu.value = '初始化导航'
+  ElMessage.success('已进入位姿初始化模式')
+}
+
+const handleConfirmReboot = async () => {
+  isBusy.value = true
+  try {
+    await startNavStandard(mapName.value)
+    ElMessage.success('导航重启指令已发送')
+  } catch (error) {
+    console.error('导航重启请求失败:', error)
+    ElMessage.error('导航重启请求失败')
+  }
+
+  setTimeout(() => {
+    if (isBusy.value) {
+      isBusy.value = false
+      ElMessage.warning('导航重启响应超时,请检查系统状态')
+    }
+  }, 15000)
+}
+
+const handleConfirmStop = async () => {
+  selectPointMode.value = false
+  initPoseMode.value = false
+  nowHandMenu.value = ''
+
+  try {
+    await ElMessageBox.confirm('请选择要执行的操作:', '操作选择', {
+      confirmButtonText: '急停',
+      cancelButtonText: '停止导航',
+      distinguishCancelAndClose: true,
+      type: 'warning',
+      customClass: 'stop-action-dialog'
+    })
+    executeEmergencyStop()
+  } catch (action) {
+    if (action === 'cancel') {
+      executeNavigationStop()
+    }
+  }
+}
+
+const executeEmergencyStop = async () => {
+  try {
+    await emergencyStop(true)
+    ElMessage.warning('急停指令已发送')
+  } catch (error) {
+    console.error('急停请求失败:', error)
+    ElMessage.error('急停请求失败')
+  }
+}
+
+const executeNavigationStop = async () => {
+  try {
+    await stopNavigation()
+    ElMessage.info('导航停止指令已发送')
+  } catch (error) {
+    console.error('导航停止请求失败:', error)
+    ElMessage.error('导航停止请求失败')
+  }
+}
+
+const executeEmergencyStopRelease = async () => {
+  try {
+    await releaseEmergencyStop()
+    ElMessage.success('解除急停指令已发送')
+  } catch (error) {
+    console.error('解除急停请求失败:', error)
+    ElMessage.error('解除急停请求失败')
+  }
+}
+
+const handleNavigationPause = async () => {
+  if (!isNavigating.value || navigationStatus.value === 'paused') {
+    ElMessage.warning('当前没有正在执行的导航任务')
+    return
+  }
+
+  try {
+    await pauseTask()
+    navigationStatus.value = 'paused'
+    ElMessage.success('导航已暂停')
+  } catch (error) {
+    console.error('暂停导航请求失败:', error)
+    ElMessage.error('暂停导航请求失败')
+  }
+}
+
+const handleNavigationResume = async () => {
+  if (!currentNavigationTask.value || navigationStatus.value !== 'paused') {
+    ElMessage.warning('没有可恢复的导航任务')
+    return
+  }
+
+  try {
+    await resumeTask()
+    navigationStatus.value = 'navigating'
+    ElMessage.success('导航已恢复')
+  } catch (error) {
+    console.error('恢复导航请求失败:', error)
+    ElMessage.error('恢复导航请求失败')
+  }
+}
+
+const handleNavigationStop = async () => {
+  if (!isNavigating.value && navigationStatus.value === 'idle') {
+    ElMessage.warning('当前没有导航任务')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm('确定要停止当前导航任务吗?', '确认停止', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    await cancelTask()
+
+    isNavigating.value = false
+    navigationStatus.value = 'idle'
+    currentNavigationTask.value = null
+
+    if (olmap.value && olmap.value.clearTrajectory) {
+      olmap.value.clearTrajectory()
+      settingParams.showRoadNetwork = true
+    }
+
+    ElMessage.success('导航任务已停止')
+  } catch {
+    // 用户取消
+  }
+}
+
+const showCreateTaskDialog = () => {
+  ElMessage.info('创建任务功能待接入')
+}
+
+// const startTask = (task) => {
+//   task.status = 'running'
+//   ElMessage.success(`任务 "${task.name}" 已开始执行`)
+// }
+
+/* const pauseTask = (task) => {
+  task.status = 'paused'
+  ElMessage.warning(`任务 "${task.name}" 已暂停`)
+}
+
+const resumeTask = (task) => {
+  task.status = 'running'
+  ElMessage.success(`任务 "${task.name}" 已继续执行`)
+}
+
+const cancelTask = async (task) => {
+  try {
+    await ElMessageBox.confirm(`确定要取消任务 "${task.name}" 吗?`, '确认取消', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    task.status = 'idle'
+    ElMessage.info(`任务 "${task.name}" 已取消`)
+  } catch {
+    // 用户取消
+  }
+}
+
+const toggleInitPoseMode = () => {
+  initPoseMode.value = !initPoseMode.value
+  selectPointMode.value = false
+  nowHandMenu.value = initPoseMode.value ? '初始化导航' : ''
+
+  if (initPoseMode.value) {
+    ElMessage.success('已进入位姿初始化模式')
+  } else {
+    ElMessage.info('已退出位姿初始化模式')
+  }
+} */
+
+const onRestart = () => {
+  handleConfirmReboot()
+}
+
+const onStop = () => {
+  handleConfirmStop()
+}
+
+const onInit = () => {
+  handleConfirmInit()
+}
+
+const onZoomIn = () => {
+  handleZoomIn()
+}
+
+const onZoomOut = () => {
+  handleZoomOut()
+}
+
+const onCenterRobot = () => {
+  handleCenterToRobot()
+}
+
+const onToggleFullscreen = () => {
+  handleToggleFullscreen()
+}
+
+const getTaskStatusText = (status) => {
+  const statusMap = {
+    'idle': '空闲',
+    'running': '执行中',
+    'paused': '暂停'
+  }
+  return statusMap[status] || '未知'
+}
+
+const onWpSelect = (waypoint) => {
+  console.log('选择目标点:', waypoint)
+  ElMessage.success(`已选择目标点: ${waypoint.name}`)
+}
+
+const onWpSend = (waypoint) => {
+  console.log('发送目标点:', waypoint)
+  ElMessage.success(`已发送目标点: ${waypoint.name}`)
+}
+
+const onWpCreate = () => {
+  console.log('创建目标点')
+  ElMessage.info('创建目标点功能待实现')
+}
+
+const onWpEdit = (waypoint) => {
+  console.log('编辑目标点:', waypoint)
+  pointEditDiaShow.value = true
+  pointEditData.id = waypoint.id
+  pointEditData.x = waypoint.x
+  pointEditData.y = waypoint.y
+  pointEditData.type = waypoint.type
+  pointEditData.actionMenuList = waypoint.action ? [...waypoint.action] : [{ value: 0, other: 0 }]
+}
+
+const onWpRemove = (waypoint) => {
+  console.log('删除目标点:', waypoint)
+  waypoints.value = waypoints.value.filter(wp => wp.id !== waypoint.id)
+  selectedWaypointIds.value = selectedWaypointIds.value.filter(id => id !== waypoint.id)
+  olmap.value?.removeIconHtmlById("calibration-" + waypoint.id)
+  ElMessage.success(`已删除目标点: ${waypoint.name || '目标点'}`)
+}
+
+const onWpMoveUp = () => {
+  if (selectedWaypointIds.value.length !== 1) return
+
+  const selectedId = selectedWaypointIds.value[0]
+  const index = waypoints.value.findIndex(wp => wp.id === selectedId)
+
+  if (index > 0) {
+    const temp = waypoints.value[index]
+    waypoints.value[index] = waypoints.value[index - 1]
+    waypoints.value[index - 1] = temp
+    ElMessage.success('目标点已上移')
+  }
+}
+
+const onWpMoveDown = () => {
+  if (selectedWaypointIds.value.length !== 1) return
+
+  const selectedId = selectedWaypointIds.value[0]
+  const index = waypoints.value.findIndex(wp => wp.id === selectedId)
+
+  if (index < waypoints.value.length - 1) {
+    const temp = waypoints.value[index]
+    waypoints.value[index] = waypoints.value[index + 1]
+    waypoints.value[index + 1] = temp
+    ElMessage.success('目标点已下移')
+  }
+}
+
+const onWpBatchRemove = async () => {
+  if (selectedWaypointIds.value.length === 0) return
+
+  try {
+    await ElMessageBox.confirm(`确定要删除选中的 ${selectedWaypointIds.value.length} 个目标点吗?`, '批量删除', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    const waypointsToRemove = waypoints.value.filter(wp => selectedWaypointIds.value.includes(wp.id))
+
+    waypointsToRemove.forEach(wp => {
+      olmap.value?.removeIconHtmlById("calibration-" + wp.id)
+    })
+
+    waypoints.value = waypoints.value.filter(wp => !selectedWaypointIds.value.includes(wp.id))
+    selectedWaypointIds.value = []
+    ElMessage.success('目标点删除成功')
+  } catch {
+    // 用户取消
+  }
+}
+
+const onWpGoto = async () => {
+  if (selectedWaypointIds.value.length !== 1) return
+
+  if (isNavigating.value) {
+    ElMessage.warning('机器人正在导航中,请等待任务完成后再发送新任务')
+    return
+  }
+
+  const selectedWaypoint = waypoints.value.find(wp => wp.id === selectedWaypointIds.value[0])
+  console.log('前往目标点:', selectedWaypoint)
+
+  if (selectedWaypoint) {
+    if (olmap.value && olmap.value.clearTrajectory) {
+      olmap.value.clearTrajectory()
+      console.log('已清除之前的轨迹')
+    }
+
+    const timestamp = new Date().getTime()
+
+    currentNavigationTask.value = {
+      waypoint: selectedWaypoint,
+      timestamp: timestamp,
+      status: 'planning'
+    }
+    lastGotoRequest.value = { roadmap: mapName.value, coord: [[Number(selectedWaypoint.x), Number(selectedWaypoint.y)]] }
+    navigationStatus.value = 'planning'
+
+    try {
+      // 发送路径规划请求
+      await requestPlanning(mapName.value, Number(selectedWaypoint.x), Number(selectedWaypoint.y))
+      // 发送前往目标点请求
+      await gotoTarget(mapName.value, Number(selectedWaypoint.x), Number(selectedWaypoint.y))
+      ElMessage.success(`正在发送前往指令: ${selectedWaypoint.name || '目标点' + selectedWaypoint.id} (${selectedWaypoint.x}, ${selectedWaypoint.y})`)
+    } catch (error) {
+      console.error('发送导航指令失败:', error)
+      ElMessage.error('发送导航指令失败')
+    }
+  }
+}
+
+const onWpGotoSingle = async (waypoint) => {
+  if (isNavigating.value) {
+    ElMessage.warning('机器人正在导航中,请等待任务完成后再发送新任务')
+    return
+  }
+
+  if (olmap.value && olmap.value.clearTrajectory) {
+    olmap.value.clearTrajectory()
+    console.log('已清除之前的轨迹')
+  }
+
+  const timestamp = new Date().getTime()
+
+  currentNavigationTask.value = {
+    waypoint: waypoint,
+    timestamp: timestamp,
+    status: 'planning'
+  }
+  lastGotoRequest.value = { roadmap: mapName.value, coord: [[Number(waypoint.x), Number(waypoint.y)]] }
+  navigationStatus.value = 'planning'
+
+  try {
+    // 发送路径规划请求
+    await requestPlanning(mapName.value, Number(waypoint.x), Number(waypoint.y))
+    // 发送前往目标点请求
+    await gotoTarget(mapName.value, Number(waypoint.x), Number(waypoint.y))
+    ElMessage.success(`正在发送前往指令: ${waypoint.name || '目标点' + waypoint.id} (${waypoint.x}, ${waypoint.y})`)
+  } catch (error) {
+    console.error('发送导航指令失败:', error)
+    ElMessage.error('发送导航指令失败')
+  }
+}
+
+const cancelCurrentNavigation = async () => {
+  if (!isNavigating.value) {
+    ElMessage.info('当前没有正在进行的导航任务')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm('确定要取消当前的导航任务吗?', '取消导航', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    navigationStatus.value = 'idle'
+    isNavigating.value = false
+    currentNavigationTask.value = null
+    lastGotoRequest.value = null
+
+    if (olmap.value && olmap.value.clearTrajectory) {
+      olmap.value.clearTrajectory()
+      settingParams.showRoadNetwork = true
+    }
+
+    ElMessage.success('导航任务已取消')
+    console.log('用户取消了导航任务')
+  } catch {
+    // 用户取消
+  }
+}
+
+const executeNextWaypoint = async () => {
+  if (!currentExecutingTask.value || !currentExecutingTask.value.points) {
+    console.error('没有正在执行的任务或任务没有目标点')
+    return
+  }
+
+  const waypointList = currentExecutingTask.value.points
+  const currentIndex = currentTaskWaypointIndex.value
+
+  if (currentIndex >= waypointList.length) {
+    console.log('所有目标点已执行完成')
+    return
+  }
+
+  const currentWaypoint = waypointList[currentIndex]
+  console.log(`执行目标点 ${currentIndex + 1}/${waypointList.length}:`, currentWaypoint)
+
+  if (olmap.value && olmap.value.clearTrajectory) {
+    olmap.value.clearTrajectory()
+    console.log('已清除之前的轨迹,准备执行下一个目标点')
+  }
+
+  const timestamp = new Date().getTime()
+
+  currentNavigationTask.value = {
+    waypoint: currentWaypoint,
+    timestamp: timestamp,
+    taskContext: {
+      taskId: currentExecutingTask.value.taskId,
+      waypointIndex: currentIndex,
+      totalWaypoints: waypointList.length
+    }
+  }
+  lastGotoRequest.value = { roadmap: mapName.value, coord: [[Number(currentWaypoint.x), Number(currentWaypoint.y)]] }
+  navigationStatus.value = 'planning'
+  isNavigating.value = true
+
+  try {
+    // 发送路径规划请求
+    await requestPlanning(mapName.value, Number(currentWaypoint.x), Number(currentWaypoint.y))
+    // 发送前往目标点请求
+    await gotoTarget(mapName.value, Number(currentWaypoint.x), Number(currentWaypoint.y))
+    ElMessage.info(`正在前往第 ${currentIndex + 1} 个目标点: ${currentWaypoint.name || '目标点' + currentWaypoint.id} (${currentWaypoint.x}, ${currentWaypoint.y})`)
+  } catch (error) {
+    console.error('发送导航指令失败:', error)
+    ElMessage.error('发送导航指令失败')
+  }
+}
+
+const handleTaskWaypointArrival = () => {
+  if (!currentExecutingTask.value) return
+
+  currentTaskWaypointIndex.value++
+  const totalWaypoints = currentExecutingTask.value.points.length
+
+  console.log(`目标点到达,进度: ${currentTaskWaypointIndex.value}/${totalWaypoints}`)
+
+  if (currentTaskWaypointIndex.value >= totalWaypoints) {
+    ElMessage.success(`任务 "${currentExecutingTask.value.taskName}" 的所有目标点已完成!`)
+    console.log('任务所有目标点执行完成')
+  } else {
+    ElMessage.success(`第 ${currentTaskWaypointIndex.value} 个目标点已到达,继续前往下一个目标点...`)
+    setTimeout(() => {
+      executeNextWaypoint()
+    }, 2000)
+  }
+}
+
+const onWpCreateTask = () => {
+  if (selectedWaypointIds.value.length === 0) {
+    ElMessage.warning('请先选择要添加到任务中的目标点')
+    return
+  }
+
+  const selectedWaypointsList = waypoints.value.filter(wp => selectedWaypointIds.value.includes(wp.id))
+  console.log(`准备使用 ${selectedWaypointsList.length} 个目标点创建任务`, selectedWaypointsList)
+
+  generateTaskParam.selectedWaypoints = selectedWaypointsList
+  taskGenerateDiaShow.value = true
+}
+
+const onWpSelectionChange = (selection) => {
+  selectedWaypointIds.value = selection.map(wp => wp.id)
+  waypointSingle.value = selection.length !== 1
+  waypointMultiple.value = selection.length === 0
+  console.log('目标点选择变更:', selectedWaypointIds.value)
+}
+
+const onMapSelectModeChange = (isActive) => {
+  selectPointMode.value = isActive
+  initPoseMode.value = false
+
+  if (isActive) {
+    ElMessage.success('已开启地图选点模式,点击地图添加目标点')
+  } else {
+    ElMessage.info('已关闭地图选点模式')
+  }
+}
+
+const clearTaskViewWaypoints = () => {
+  if (taskViewAutoCloseTimer.value) {
+    clearTimeout(taskViewAutoCloseTimer.value)
+    taskViewAutoCloseTimer.value = null
+  }
+
+  if (olmap.value && currentViewTaskWaypointIds.value.length > 0) {
+    currentViewTaskWaypointIds.value.forEach(waypointId => {
+      if (olmap.value.removeIconHtmlById) {
+        olmap.value.removeIconHtmlById(waypointId)
+      }
+    })
+    currentViewTaskWaypointIds.value = []
+    console.log('已清除任务查看标记点')
+  }
+}
+
+const onTaskView = async (task) => {
+  console.log('查看任务详情:', task)
+
+  try {
+    clearTaskViewWaypoints()
+
+    const response = await getTask(mapName.value, task.taskName)
+    console.log('任务详情响应:', response)
+
+    if (response && response.coord && response.coord.length > 0) {
+      const waypointList = response.coord.map((coord, index) => ({
+        id: index + 1,
+        name: `点位${index + 1}`,
+        x: coord[0],
+        y: coord[1]
+      }))
+
+      waypointList.forEach((waypoint, index) => {
+        if (olmap.value && olmap.value.addHtmlIcon) {
+          const displayId = index + 1
+          olmap.value.addHtmlIcon(displayId, waypoint.x, waypoint.y, '', 'task-view')
+          currentViewTaskWaypointIds.value.push(`calibration-${displayId}`)
+        }
+      })
+
+      taskViewAutoCloseTimer.value = setTimeout(() => {
+        clearTaskViewWaypoints()
+      }, 10000)
+
+      console.log(`显示任务点位:`, waypointList)
+    } else {
+      ElMessage.warning('该任务没有包含点位数据')
+    }
+  } catch (error) {
+    console.error('加载任务详情失败:', error)
+    ElMessage.error('加载任务详情失败: ' + (error.message || '未知错误'))
+  }
+}
+
+const onTaskEdit = async (task) => {
+  console.log('编辑任务:', task)
+
+  try {
+    const response = await getTask(mapName.value, task.taskName)
+    console.log('任务详情响应:', response)
+
+    if (response) {
+      editTaskParam.originalTaskName = task.taskName
+      editTaskParam.taskId = task.taskId
+      editTaskParam.taskName = task.taskName
+      editTaskParam.count = response.repeate || 1
+
+      if (response.cron) {
+        const cronParts = response.cron.split(' ')
+        if (cronParts.length >= 7) {
+          const minutes = String(cronParts[1]).padStart(2, '0')
+          const hours = String(cronParts[2]).padStart(2, '0')
+          const weekdays = cronParts[5]
+
+          editTaskParam.time = `${hours}:${minutes}:00`
+
+          if (weekdays !== '*' && weekdays !== '?') {
+            editTaskParam.date = weekdays.split(',').filter(d => d)
+          } else {
+            editTaskParam.date = []
+          }
+        } else if (cronParts.length >= 5) {
+          const minutes = String(cronParts[0]).padStart(2, '0')
+          const hours = String(cronParts[1]).padStart(2, '0')
+          const weekdays = cronParts[4]
+
+          editTaskParam.time = `${hours}:${minutes}:00`
+
+          if (weekdays !== '*' && weekdays !== '?') {
+            editTaskParam.date = weekdays.split(',').filter(d => d)
+          } else {
+            editTaskParam.date = []
+          }
+        }
+      } else {
+        editTaskParam.time = ''
+        editTaskParam.date = []
+      }
+
+      editTaskParam.selectedWaypoints = (response.coord || []).map((coord, index) => ({
+        id: index + 1,
+        name: `目标点${index + 1}`,
+        x: coord[0].toString(),
+        y: coord[1].toString(),
+        type: response.plan && response.plan[index] === 'route' ? 1 : 0,
+        action: response.action && response.action[index] ?
+          convertApiActionToEditFormat(response.action[index]) :
+          [{ value: 0, other: 0 }]
+      }))
+
+      taskEditDiaShow.value = true
+    } else {
+      ElMessage.error('获取任务详情失败')
+    }
+  } catch (error) {
+    console.error('加载任务详情失败:', error)
+    ElMessage.error('加载任务详情失败: ' + (error.message || '未知错误'))
+  }
+}
+
+const convertApiActionToEditFormat = (actions) => {
+  if (!actions || actions.length === 0) {
+    return [{ value: 0, other: 0 }]
+  }
+
+  return actions.map(action => {
+    const actionTypeMap = {
+      'wait': 0,
+      'start_record': 1,
+      'stop_record': 2,
+      'add_trajectory': 3,
+      'hook_mount': 4,
+      'hook_unmount': 5
+    }
+
+    const value = actionTypeMap[action.type] || 0
+    const result = { value }
+
+    if (action.type === 'wait' && action.duration !== undefined) {
+      result.other = action.duration
+    }
+
+    return result
+  })
+}
+
+const closeTaskEdit = () => {
+  taskEditDiaShow.value = false
+  resetEditTaskParam()
+}
+
+const resetEditTaskParam = () => {
+  editTaskParam.originalTaskName = ''
+  editTaskParam.taskId = ''
+  editTaskParam.taskName = ''
+  editTaskParam.count = 1
+  editTaskParam.time = ''
+  editTaskParam.date = []
+  editTaskParam.selectedWaypoints = []
+}
+
+const submitTaskEdit = async () => {
+  const orderedPoints = editTaskParam.selectedWaypoints || []
+
+  if (!editTaskParam.taskName) {
+    ElMessage.warning('请输入任务名称')
+    return
+  }
+
+  if (orderedPoints.length === 0) {
+    ElMessage.warning('任务必须包含至少一个目标点')
+    return
+  }
+
+  try {
+    const coordinates = orderedPoints.map(point => [
+      parseFloat(point.x),
+      parseFloat(point.y),
+      0
+    ])
+
+    const plan = orderedPoints.map(point => {
+      return point.type === 1 ? 'route' : 'free'
+    })
+
+    let cron = ''
+    if (editTaskParam.time && editTaskParam.date.length > 0) {
+      let hours, minutes
+      if (typeof editTaskParam.time === 'string') {
+        const timeParts = editTaskParam.time.split(':')
+        hours = timeParts[0]
+        minutes = timeParts[1]
+      } else {
+        const timeObj = new Date(editTaskParam.time)
+        hours = String(timeObj.getHours()).padStart(2, '0')
+        minutes = String(timeObj.getMinutes()).padStart(2, '0')
+      }
+      const weekdays = editTaskParam.date.sort().join(',')
+      cron = `00 ${minutes} ${hours} ? * ${weekdays} *`
+    }
+
+    const taskData = {
+      taskId: editTaskParam.taskId,
+      map: mapName.value,
+      taskName: editTaskParam.taskName,
+      path: editTaskParam.taskName,
+      coord_type: 'local',
+      cron: cron,
+      executeTime: editTaskParam.time || null,
+      executeWeekdays: editTaskParam.date.join(',') || null,
+      coord: coordinates,
+      plan: plan,
+      action: [[]],
+      repeate: editTaskParam.count || 1
+    }
+
+    const response = await updateTask(taskData)
+    console.log("更新任务响应:", response)
+
+    if (response.status === true) {
+      ElMessage.success('任务更新成功')
+      await loadTaskList()
+      closeTaskEdit()
+    } else {
+      ElMessage.error('更新任务失败: ' + (response.msg || '未知错误'))
+    }
+  } catch (error) {
+    console.error("更新任务失败:", error)
+      ElMessage.error('更新任务失败: ' + (error.message || '未知错误'))
+  }
+}
+
+const onTaskStart = async (task) => {
+  console.log('开始任务:', task)
+
+  if (isTaskExecuting.value) {
+    ElMessage.warning('已有任务正在执行,请先完成或取消当前任务')
+    return
+  }
+
+  try {
+    const response = await getTask(mapName.value, task.taskName)
+    console.log('任务详情响应:', response)
+
+    if (response && response.coord && response.coord.length > 0) {
+      const waypointList = response.coord.map((coord, index) => ({
+        id: index + 1,
+        name: `目标点${index + 1}`,
+        x: coord[0],
+        y: coord[1],
+        type: response.plan && response.plan[index] === 'route' ? 1 : 0,
+        action: response.action && response.action[index] ? response.action[index] : []
+      }))
+
+      task.points = waypointList
+      task.totalWaypoints = waypointList.length
+
+      const timestamp = new Date().getTime()
+
+      currentExecutingTask.value = task
+      currentTaskWaypointIndex.value = 0
+      lastTaskExecRequest.value = { roadmap: mapName.value, task: task.taskName }
+      isTaskExecuting.value = true
+      taskExecutionStatus.value = 'executing'
+
+      task.status = 'running'
+      task.executionStatus = 'executing'
+
+      // 发送启动任务请求
+      await startTask(mapName.value, task.taskName)
+
+      ElMessage.success(`正在启动任务 "${task.taskName}",包含 ${waypointList.length} 个目标点`)
+    } else {
+      ElMessage.error('任务没有包含目标点,无法执行')
+    }
+  } catch (error) {
+    console.error('获取任务详情失败:', error)
+    ElMessage.error('获取任务详情失败: ' + (error.message || '未知错误'))
+  }
+}
+
+const onTaskPause = async (task) => {
+  console.log('暂停任务:', task)
+
+  if (task.status !== 'running') {
+    ElMessage.warning('该任务当前未在执行中')
+    return
+  }
+
+  try {
+    await pauseTask()
+    task.status = 'paused'
+    ElMessage.info('任务已暂停')
+  } catch (error) {
+    console.error('暂停任务失败:', error)
+    ElMessage.error('暂停任务失败')
+  }
+}
+
+const onTaskStop = async (task) => {
+  console.log('停止任务:', task)
+
+  if (!isTaskExecuting.value || currentExecutingTask.value?.taskId !== task.taskId) {
+    ElMessage.warning('该任务当前未在执行中')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(`确定要取消任务 "${task.taskName}" 吗?`, '取消任务', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    await cancelTask()
+
+    console.log("发送取消任务请求成功")
+  } catch {
+    // 用户取消
+  }
+}
+
+const onTaskRemove = async (task) => {
+  console.log('删除任务:', task)
+
+  if (isTaskExecuting.value && currentExecutingTask.value?.taskId === task.taskId) {
+    ElMessage.warning('任务正在执行中,无法删除。请先停止任务。')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(`确定要删除任务 "${task.taskName}" 吗?`, '删除任务', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    try {
+      const response = await delTask({
+        map: mapName.value,
+        path: task.taskName,
+        taskId: task.taskId || null
+      })
+
+      if (response.status === true) {
+        ElMessage.success(`任务 "${task.taskName}" 已删除`)
+        await loadTaskList()
+      } else {
+        ElMessage.error('删除任务失败: ' + (response.msg || '未知错误'))
+      }
+    } catch (error) {
+      console.error("删除任务失败:", error)
+      ElMessage.error('删除任务失败: ' + (error.message || '未知错误'))
+    }
+  } catch {
+    // 用户取消
+  }
+}
+
+const onTaskResume = async (task) => {
+  console.log('继续任务:', task)
+
+  if (task.status !== 'paused') {
+    ElMessage.warning('该任务当前不在暂停状态')
+    return
+  }
+
+  try {
+    await resumeTask()
+    task.status = 'running'
+    ElMessage.success('任务已继续')
+  } catch (error) {
+    console.error('继续任务失败:', error)
+    ElMessage.error('继续任务失败')
+  }
+}
+
+const getTaskStatusTextByStatus = (status) => {
+  const statusMap = {
+    0: '运行中',
+    1: '空闲',
+    'idle': '空闲',
+    'running': '运行中',
+    'paused': '暂停',
+    'completed': '已完成',
+    'error': '失败'
+  }
+  return statusMap[status] || '未知'
+}
+
+const getTaskStatusClass = (status) => {
+  const statusClassMap = {
+    0: 'status-running',
+    1: 'status-idle',
+    'idle': 'status-idle',
+    'running': 'status-running',
+    'paused': 'status-paused',
+    'completed': 'status-completed',
+    'error': 'status-error'
+  }
+  return statusClassMap[status] || 'status-unknown'
+}
+
+const formatTime = (time) => {
+  if (!time) return '--'
+  if (typeof time === 'string') return time
+  if (time instanceof Date) {
+    return time.toLocaleTimeString('zh-CN', {
+      hour12: false,
+      hour: '2-digit',
+      minute: '2-digit'
+    })
+  }
+  return '--'
+}
+
+const formatDate = (dateArray) => {
+  if (!dateArray || !Array.isArray(dateArray) || dateArray.length === 0) return '--'
+
+  const dayNames = {
+    '1': '周一',
+    '2': '周二',
+    '3': '周三',
+    '4': '周四',
+    '5': '周五',
+    '6': '周六',
+    '7': '周日'
+  }
+
+  return dateArray.map(day => dayNames[day] || day).join(', ')
+}
+
+const formatSeconds = (seconds) => {
+  if (!seconds || seconds < 0) return '00:00:00'
+
+  const hours = Math.floor(seconds / 3600)
+  const minutes = Math.floor((seconds % 3600) / 60)
+  const secs = Math.floor(seconds % 60)
+
+  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
+}
+
+const onSettingChange = (setting) => {
+  console.log('功能设置变更:', setting)
+  settingParams[setting.key] = setting.value
+
+  switch(setting.key) {
+    case 'pointCloud':
+      ElMessage.info(`点云显示已${setting.value ? '开启' : '关闭'}`)
+      break
+    case 'baseMap':
+      ElMessage.info(`底图显示已${setting.value ? '开启' : '关闭'}`)
+      break
+    case 'pointId':
+      ElMessage.info(`点ID显示已${setting.value ? '开启' : '关闭'}`)
+      break
+    case 'follow':
+      ElMessage.info(`位置跟随已${setting.value ? '开启' : '关闭'}`)
+      break
+    case 'network':
+      ElMessage.info(`网络邻居显示已${setting.value ? '开启' : '关闭'}`)
+      break
+  }
+}
+
+const loadTaskList = async () => {
+  try {
+    const response = await listTask(mapName.value)
+    console.log("任务列表:",response);
+    
+    if (response.status && response.paths && Array.isArray(response.paths)) {
+      const oldTasks = tasks.value || []
+
+      // 新的API返回格式: paths数组每个元素是包含完整信息的对象
+      const taskPromises = response.paths.map(async (taskItem) => {
+        const taskName = taskItem.taskName || taskItem.path
+        const existingTask = oldTasks.find(t => t.taskName === taskName)
+
+        let taskObj = {
+          taskId: taskItem.taskId || 0,
+          taskName: taskName,
+          status: taskItem.status || 'idle',
+          count: taskItem.repeatCount || taskItem.repeate || 1,
+          time: taskItem.executeTime || '',
+          date: taskItem.executeWeekdays ? taskItem.executeWeekdays.split(',') : [],
+          cron: taskItem.cron || '',
+          points: [],
+          executionStatus: 'idle',
+          currentWaypointIndex: 0,
+          totalWaypoints: 0,
+          scheduledTime: null,
+          scheduledTimeStr: '',
+          scheduledWeekDays: [],
+          shouldRunToday: false
+        }
+
+        // 解析cron表达式获取定时信息
+        if (taskItem.cron) {
+          const cronInfo = parseCronExpression(taskItem.cron)
+          taskObj.scheduledTimeStr = cronInfo.timeStr || taskObj.time
+          taskObj.scheduledWeekDays = cronInfo.weekDays
+
+          if (cronInfo.hours !== null && cronInfo.minutes !== null) {
+            const scheduledTime = new Date()
+            scheduledTime.setHours(cronInfo.hours, cronInfo.minutes, 0, 0)
+            taskObj.scheduledTime = scheduledTime
+          }
+
+          const currentDay = new Date().getDay() || 7
+          taskObj.shouldRunToday = cronInfo.weekDays.includes(String(currentDay))
+        }
+
+        // 解析目标点坐标
+        if (taskItem.coord && Array.isArray(taskItem.coord)) {
+          taskObj.points = taskItem.coord.map((coord, idx) => ({
+            id: idx + 1,
+            x: coord[0],
+            y: coord[1],
+            z: coord[2] || 0,
+            type: taskItem.plan && taskItem.plan[idx] === 'route' ? 1 : 0,
+            action: taskItem.action && taskItem.action[idx] || []
+          }))
+          taskObj.totalWaypoints = taskObj.points.length
+        }
+
+        // 保留运行中任务的执行状态
+        if (currentExecutingTask.value && currentExecutingTask.value.taskName === taskName) {
+          taskObj.status = currentExecutingTask.value.status || 'running'
+          taskObj.executionStatus = currentExecutingTask.value.executionStatus || 'executing'
+          taskObj.currentWaypointIndex = currentTaskWaypointIndex.value || 0
+          if (currentExecutingTask.value.points) {
+            taskObj.points = currentExecutingTask.value.points
+          }
+        } else if (existingTask && existingTask.status !== 'idle') {
+          console.log(`保留任务状态: ${taskName} - ${existingTask.status}`)
+          taskObj.status = existingTask.status
+          taskObj.executionStatus = existingTask.executionStatus
+          taskObj.currentWaypointIndex = existingTask.currentWaypointIndex
+        }
+
+        return taskObj
+      })
+
+      tasks.value = await Promise.all(taskPromises)
+      console.log("任务列表加载成功,包含完整cron配置:", tasks.value.length, "个任务")
+      calculateTodayScheduledTasks()
+    } else {
+      console.error("加载任务列表失败,响应:", response)
+      tasks.value = []
+    }
+  } catch (error) {
+    console.error("加载任务列表异常:", error)
+    tasks.value = []
+  }
+}
+
+const parseCronExpression = (cronStr) => {
+  const result = {
+    timeStr: '',
+    hours: null,
+    minutes: null,
+    weekDays: []
+  }
+
+  if (!cronStr) return result
+
+  try {
+    const parts = cronStr.split(' ')
+
+    if (parts.length >= 6) {
+      const minutes = parts[1]
+      const hours = parts[2]
+      const weekdays = parts[5]
+
+      if (hours !== '*' && hours !== '?' && minutes !== '*' && minutes !== '?') {
+        result.hours = parseInt(hours)
+        result.minutes = parseInt(minutes)
+        result.timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`
+      }
+
+      if (weekdays !== '*' && weekdays !== '?') {
+        result.weekDays = weekdays.split(',').map(d => d.trim()).filter(d => d)
+      }
+    } else if (parts.length >= 5) {
+      const minutes = parts[0]
+      const hours = parts[1]
+      const weekdays = parts[4]
+
+      if (hours !== '*' && hours !== '?' && minutes !== '*' && minutes !== '?') {
+        result.hours = parseInt(hours)
+        result.minutes = parseInt(minutes)
+        result.timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`
+      }
+
+      if (weekdays !== '*' && weekdays !== '?') {
+        result.weekDays = weekdays.split(',').map(d => d.trim()).filter(d => d)
+      }
+    }
+  } catch (error) {
+    console.error("解析cron表达式失败:", cronStr, error)
+  }
+
+  return result
+}
+
+const startTaskListTimer = () => {
+  stopTaskListTimer()
+
+  taskListTimer.value = setInterval(() => {
+    loadTaskList()
+  }, 5000)
+
+  console.log('任务列表定时刷新已启动(每5秒)')
+}
+
+const stopTaskListTimer = () => {
+  if (taskListTimer.value) {
+    clearInterval(taskListTimer.value)
+    taskListTimer.value = null
+    console.log('任务列表定时刷新已停止')
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.point-edit-span {
+  display: block;
+  margin: 10px 0;
+  font-weight: bold;
+}
+
+.drawer {
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 100%;
+  border-radius: 0 0 12px 0;
+  border-left: 1px solid #F0F0F0;
+  padding: 8px 15px;
+  border-right: 1px solid #ececec;
+  border-bottom: 1px solid #ececec;
+  overflow-y: auto;
+  background-color: #fff;
+  z-index: 1000;
+}
+
+.drawer-close {
+  position: absolute;
+  right: 3px;
+  top: 3px;
+  cursor: pointer;
+}
+
+.drawer-title {
+  position: absolute;
+  top: -23px;
+  left: -8px;
+  font-size: 13px;
+  font-weight: bold;
+  color: #838383;
+}
+
+.drawer p {
+  font-size: 13px;
+  border-left: 5px #D1D1D1 solid;
+  padding-left: 5px;
+  margin: 8px 0;
+  border-radius: 3px 0 0 3px;
+}
+
+.img-container {
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  border-radius: 7px;
+  background: linear-gradient(135deg, #00bcd4, #009688);
+  cursor: pointer;
+  width: 70%;
+  aspect-ratio: 1;
+  margin-top: 10px;
+}
+
+.img-container:hover {
+  transform: scale(1.02);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
+  background-color: #00796b;
+}
+
+.img-container:active {
+  transform: scale(0.98);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+  background-color: #004d40;
+}
+
+.img-container img {
+  opacity: 1;
+}
+
+.img-container span {
+  font-size: 12px;
+  color: #ffffff;
+  font-weight: bold;
+}
+
+.img-container.active {
+  background: linear-gradient(135deg, #007d8d, #004b43);
+}
+
+.explore-unit {
+  margin-left: 8px;
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px 20px 0 20px;
+}
+
+:deep(.download-map .el-dialog__body) {
+  padding: 10px 20px 0 20px !important;
+}
+
+:deep(.el-table--medium .el-table__cell) {
+  padding: 6px 0;
+}
+
+:deep(.action-menu .action-menu_input .el-input__inner) {
+  padding: 0 5px;
+}
+
+.task-status-tag {
+  margin-left: 20px;
+}
+
+:deep(.drawer .el-collapse-item__header) {
+  height: 38px;
+  line-height: 38px;
+  color: #767676;
+  font-weight: bold;
+}
+
+:deep(.drawer .el-collapse-item__content) {
+  text-align: left;
+  padding-bottom: 10px;
+}
+
+:deep(.drawer .el-collapse) {
+  border: 1px solid #EBEEF5;
+  padding: 0 8px;
+  border-radius: 5px;
+}
+
+.collapse-content-div {
+  margin-top: 0;
+}
+
+:deep(.collapse-content-div .el-button--mini) {
+  padding: 4px 10px;
+}
+
+.hand-ment-mark {
+  position: absolute;
+  bottom: 12px;
+  left: 12px;
+  z-index: 1000;
+}
+
+.obstacle-warning {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  z-index: 2000;
+  min-width: 320px;
+  max-width: 400px;
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+  backdrop-filter: blur(10px);
+  animation: slideInRight 0.3s ease-out;
+
+  &.obstacle-info {
+    background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(99, 102, 241, 0.95) 100%);
+    border: 1px solid rgba(59, 130, 246, 0.3);
+  }
+
+  &.obstacle-warning {
+    background: linear-gradient(135deg, rgba(245, 158, 11, 0.95) 0%, rgba(251, 191, 36, 0.95) 100%);
+    border: 1px solid rgba(245, 158, 11, 0.3);
+  }
+
+  &.obstacle-danger {
+    background: linear-gradient(135deg, rgba(239, 68, 68, 0.95) 0%, rgba(248, 113, 113, 0.95) 100%);
+    border: 1px solid rgba(239, 68, 68, 0.3);
+    animation: pulse 2s infinite;
+  }
+
+  .warning-content {
+    display: flex;
+    align-items: flex-start;
+    padding: 16px 20px;
+    color: white;
+
+    .warning-icon {
+      margin-right: 12px;
+      margin-top: 2px;
+      flex-shrink: 0;
+
+      i {
+        font-size: 20px;
+        color: white;
+      }
+    }
+
+    .warning-text {
+      flex: 1;
+
+      .warning-title {
+        font-size: 14px;
+        font-weight: 600;
+        margin-bottom: 4px;
+        color: white;
+      }
+
+      .warning-message {
+        font-size: 13px;
+        line-height: 1.4;
+        margin-bottom: 8px;
+        color: rgba(255, 255, 255, 0.95);
+      }
+
+      .obstacle-distances {
+        display: flex;
+        gap: 12px;
+        flex-wrap: wrap;
+
+        .distance-item {
+          font-size: 11px;
+          background: rgba(255, 255, 255, 0.2);
+          padding: 2px 8px;
+          border-radius: 12px;
+          color: white;
+          font-weight: 500;
+          border: 1px solid rgba(255, 255, 255, 0.3);
+        }
+      }
+    }
+
+    .warning-close {
+      margin-left: 8px;
+      margin-top: 2px;
+      cursor: pointer;
+      padding: 4px;
+      border-radius: 50%;
+      transition: all 0.2s ease;
+      flex-shrink: 0;
+
+      &:hover {
+        background: rgba(255, 255, 255, 0.2);
+      }
+
+      i {
+        font-size: 16px;
+        color: white;
+      }
+    }
+  }
+}
+
+@keyframes slideInRight {
+  from {
+    transform: translateX(100%);
+    opacity: 0;
+  }
+  to {
+    transform: translateX(0);
+    opacity: 1;
+  }
+}
+
+@keyframes pulse {
+  0%, 100% {
+    transform: scale(1);
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+  }
+  50% {
+    transform: scale(1.02);
+    box-shadow: 0 12px 32px rgba(239, 68, 68, 0.3);
+  }
+}
+
+.obstacle-stop-warning {
+  position: absolute;
+  top: 80px;
+  right: 20px;
+  z-index: 2000;
+  min-width: 380px;
+  border-radius: 12px;
+  background: white;
+  box-shadow: 0 8px 32px rgba(239, 68, 68, 0.2);
+  border: 2px solid #FF5555;
+  animation: slideInRight 0.3s ease-out, breathe 2s infinite ease-in-out;
+
+  .stop-warning-content {
+    display: flex;
+    align-items: center;
+    padding: 16px;
+    gap: 16px;
+
+    .countdown-circle {
+      flex-shrink: 0;
+      width: 80px;
+      height: 80px;
+      border-radius: 50%;
+      background: linear-gradient(135deg, #FF5555 0%, #FF8080 100%);
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      color: white;
+      box-shadow: 0 4px 12px rgba(255, 85, 85, 0.3);
+
+      .countdown-number {
+        font-size: 28px;
+        font-weight: bold;
+        line-height: 1;
+      }
+
+      .countdown-label {
+        font-size: 12px;
+        margin-top: 4px;
+        opacity: 0.9;
+      }
+    }
+
+    .stop-warning-text {
+      flex: 1;
+
+      .stop-warning-title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #FF5555;
+        margin-bottom: 6px;
+      }
+
+      .stop-warning-message {
+        font-size: 13px;
+        color: #666;
+        line-height: 1.4;
+        margin-bottom: 6px;
+      }
+
+      .stop-warning-state {
+        font-size: 12px;
+        color: #999;
+      }
+    }
+
+    .stop-warning-actions {
+      flex-shrink: 0;
+
+      .el-button {
+        padding: 8px 16px;
+        font-size: 13px;
+      }
+    }
+  }
+}
+
+@keyframes breathe {
+  0%, 100% {
+    box-shadow: 0 8px 32px rgba(239, 68, 68, 0.2);
+  }
+  50% {
+    box-shadow: 0 12px 40px rgba(239, 68, 68, 0.35);
+  }
+}
+
+.notification__title {
+  font-size: 1.2rem;
+}
+
+.navigation-container {
+  width: 100%;
+  min-height: calc(100vh - 84px);
+  overflow: hidden;
+  position: relative;
+  background: var(--color-bg-secondary);
+
+  .map-stage {
+    position: relative;
+    width: 100%;
+    height: calc(100vh - 84px);
+    min-height: 600px;
+    overflow: hidden;
+    background: var(--color-bg-secondary);
+  }
+}
+
+.nav-toolbar {
+  position: absolute;
+  left: 16px;
+  top: 96px;
+  z-index: 50;
+}
+
+.main-menu {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+:deep(.waypoint-edit-dialog) {
+  .el-dialog {
+    border-radius: 12px !important;
+    overflow: hidden !important;
+    margin-top: 0 !important;
+    margin-bottom: 0 !important;
+    position: fixed !important;
+    top: 50% !important;
+    left: 50% !important;
+    transform: translate(-50%, -50%) !important;
+  }
+
+  .el-dialog__header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 20px 24px 16px;
+    margin: 0;
+    border-radius: 12px 12px 0 0 !important;
+
+    .el-dialog__title {
+      color: white;
+      font-weight: 600;
+      font-size: 16px;
+    }
+
+    .el-dialog__close {
+      color: white;
+      font-size: 18px;
+
+      &:hover {
+        color: #f0f0f0;
+      }
+    }
+  }
+
+  .el-dialog__body {
+    padding: 24px;
+    background: #f8fafc;
+    margin: 0 !important;
+  }
+
+  .el-dialog__footer {
+    padding: 16px 24px 24px;
+    background: #f8fafc;
+    border-top: 1px solid #e2e8f0;
+    border-radius: 0 0 12px 12px !important;
+    margin: 0 !important;
+  }
+}
+
+.dialog-content {
+  .form-section {
+    background: white;
+    border-radius: 10px;
+    padding: 24px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    border: 1px solid #f1f5f9;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      margin: 0 0 20px 0;
+      font-size: 15px;
+      font-weight: 600;
+      color: #2d3748;
+      border-bottom: 2px solid #e2e8f0;
+      padding-bottom: 10px;
+
+      i {
+        margin-right: 10px;
+        color: #667eea;
+        font-size: 18px;
+      }
+
+      .add-action-btn {
+        margin-left: 12px !important;
+        display: inline-flex !important;
+        align-items: center !important;
+        padding: 4px 8px !important;
+
+        i {
+          margin-right: 4px !important;
+          font-size: 12px !important;
+          color: #667eea !important;
+        }
+
+        span {
+          font-size: 12px !important;
+          color: #667eea !important;
+        }
+      }
+    }
+  }
+
+  .waypoint-form {
+    .el-form-item {
+      margin-bottom: 18px;
+      display: flex !important;
+      align-items: center !important;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .el-form-item__content {
+        flex: 1 !important;
+        margin-left: 0 !important;
+      }
+    }
+
+    .coordinate-input {
+      margin-bottom: 4px;
+    }
+
+    .el-form-item__label {
+      font-weight: 600 !important;
+      color: #2d3748 !important;
+      padding-right: 12px !important;
+      min-width: 100px !important;
+      font-size: 14px !important;
+      line-height: 44px !important;
+      height: 44px !important;
+      display: flex !important;
+      align-items: center !important;
+      margin-bottom: 0 !important;
+    }
+
+    .coordinate-input {
+      .el-input__inner {
+        border-radius: 8px !important;
+        border: 1px solid #e2e8f0 !important;
+        height: 44px !important;
+        font-size: 15px !important;
+        padding: 0 16px !important;
+        background: #ffffff !important;
+        transition: all 0.2s ease !important;
+
+        &:focus {
+          border-color: #667eea !important;
+          box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
+          background: #fafbff !important;
+        }
+
+        &:hover {
+          border-color: #cbd5e0 !important;
+        }
+      }
+    }
+  }
+}
+</style>

+ 248 - 0
src/views/map/vslam/index.vue

@@ -0,0 +1,248 @@
+<template>
+  <div class="vslam-container">
+    <div class="header">
+      <h3>地图: {{ mapName }}</h3>
+      <div class="controls">
+        <el-button type="primary" :icon="VideoPlay" :loading="isPlaying" @click="handlePlay">
+          {{ isPlaying ? '播放中' : '开始预览' }}
+        </el-button>
+        <el-button :icon="Download" @click="handleDownload">下载点云</el-button>
+        <el-button @click="handleRefresh">刷新统计</el-button>
+      </div>
+    </div>
+
+    <div class="content">
+      <div class="map-area">
+        <OlMap
+          ref="olMapRef"
+          :map-name="mapName"
+          :robot-pose-data="robotPoseData"
+          :show-road-network="false"
+          :show-point-cloud="showPointCloud"
+          @point-cloud-load-end="handlePointCloudLoadEnd"
+        />
+      </div>
+
+      <div class="stats-panel">
+        <h4>实时统计</h4>
+        <div class="stat-grid">
+          <div class="stat-item">
+            <span class="label">关键帧</span>
+            <span class="value">{{ statsData.keyFrameCount || 0 }}</span>
+          </div>
+          <div class="stat-item">
+            <span class="label">地图点数</span>
+            <span class="value">{{ (statsData.mapPoints || 0).toLocaleString() }}</span>
+          </div>
+          <div class="stat-item">
+            <span class="label">机器人位置</span>
+            <span class="value">
+              {{ robotPose.x.toFixed(2) }}, {{ robotPose.y.toFixed(2) }}
+            </span>
+          </div>
+          <div class="stat-item">
+            <span class="label">朝向角</span>
+            <span class="value">{{ (robotPose.heading || 0).toFixed(2) }}°</span>
+          </div>
+          <div class="stat-item">
+            <span class="label">定位状态</span>
+            <span class="value" :class="statsData.localizationStatus === 2 ? 'success' : 'warning'">
+              {{ getLocalizationStatusText(statsData.localizationStatus) }}
+            </span>
+          </div>
+          <div class="stat-item">
+            <span class="label">匹配分数</span>
+            <span class="value">{{ statsData.matchScore || 0 }}</span>
+          </div>
+        </div>
+
+        <h4>点云信息</h4>
+        <div class="point-cloud-info">
+          <el-checkbox v-model="showPointCloud">显示点云</el-checkbox>
+          <el-slider
+            v-model="pointCloudSize"
+            :min="1"
+            :max="10"
+            :step="0.5"
+            label="点云大小"
+          />
+          <span>点云大小: {{ pointCloudSize }}</span>
+          <el-progress
+            :percentage="pointCloudProgress"
+            :status="pointCloudProgress >= 100 ? 'success' : undefined"
+          />
+          <span>{{ pointCloudProgress >= 100 ? '点云加载完成' : '点云加载中...' }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { VideoPlay, Download } from '@element-plus/icons-vue'
+import OlMap from '@/components/OlMap/index.vue'
+import { useWebSocket } from '@/composables/useWebSocket'
+import * as mapApi from '@/api/robot/map'
+
+const route = useRoute()
+const router = useRouter()
+
+const mapName = ref(decodeURIComponent(route.params.mapId || ''))
+const olMapRef = ref(null)
+const isPlaying = ref(false)
+const showPointCloud = ref(true)
+const pointCloudSize = ref(2)
+const pointCloudProgress = ref(0)
+const statsData = ref({})
+const robotPose = ref({ x: 0, y: 0, heading: 0 })
+const robotPoseData = computed(() => robotPose.value)
+
+const deviceId = 'ld000001'
+let statsTimer = null
+
+const { robotPose: wsPose } = useWebSocket(deviceId, {
+  onPose: (data) => {
+    const pose = data.data
+    if (pose?.xyz) {
+      robotPose.value = { x: pose.xyz[0], y: pose.xyz[1], heading: pose.heading }
+    }
+  }
+})
+
+function getLocalizationStatusText(status) {
+  const map = { 0: '初始化', 1: '追踪中', 2: '已定位', 3: '丢失' }
+  return map[status] || '未知'
+}
+
+async function fetchStats() {
+  try {
+    const res = await mapApi.getVSlamStatistics(mapName.value)
+    if (res.code === 200) {
+      statsData.value = res.data || {}
+    }
+  } catch (e) {
+    console.error('获取VSLAM统计失败:', e)
+  }
+}
+
+function handlePlay() {
+  isPlaying.value = !isPlaying.value
+  if (isPlaying.value) {
+    statsTimer = setInterval(fetchStats, 2000)
+    ElMessage.success('开始预览')
+  } else {
+    if (statsTimer) {
+      clearInterval(statsTimer)
+      statsTimer = null
+    }
+    ElMessage.info('停止预览')
+  }
+}
+
+async function handleDownload() {
+  try {
+    ElMessage.info('正在请求点云数据...')
+    const blob = await mapApi.getPointcloud()
+    const url = URL.createObjectURL(blob)
+    const a = document.createElement('a')
+    a.href = url
+    a.download = `${mapName.value}.pcd`
+    a.click()
+    URL.revokeObjectURL(url)
+    ElMessage.success('下载完成')
+  } catch (e) {
+    ElMessage.error('下载失败')
+  }
+}
+
+function handleRefresh() {
+  fetchStats()
+  ElMessage.success('已刷新')
+}
+
+function handlePointCloudLoadEnd(totalPoints) {
+  pointCloudProgress.value = 100
+}
+
+onMounted(async () => {
+  if (!mapName.value) {
+    ElMessage.error('未指定地图')
+    router.back()
+    return
+  }
+  await fetchStats()
+  if (showPointCloud.value && olMapRef.value) {
+    olMapRef.value.loadPointCloud()
+  }
+})
+
+onBeforeUnmount(() => {
+  if (statsTimer) {
+    clearInterval(statsTimer)
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.vslam-container {
+  height: calc(100vh - 84px);
+  display: flex;
+  flex-direction: column;
+}
+.header {
+  padding: 12px 20px;
+  background: var(--el-bg-color);
+  border-bottom: 1px solid var(--el-border-color);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  h3 { margin: 0; }
+}
+.content {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+}
+.map-area {
+  flex: 1;
+  position: relative;
+}
+.stats-panel {
+  width: 320px;
+  background: var(--el-bg-color);
+  border-left: 1px solid var(--el-border-color);
+  padding: 16px;
+  overflow-y: auto;
+  h4 {
+    margin: 0 0 12px;
+    font-size: 14px;
+    color: var(--el-text-color-primary);
+    border-bottom: 1px solid var(--el-border-color-light);
+    padding-bottom: 8px;
+  }
+}
+.stat-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12px;
+  margin-bottom: 20px;
+}
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  .label { font-size: 12px; color: var(--el-text-color-secondary); }
+  .value { font-size: 14px; font-weight: 500; }
+  .success { color: var(--el-color-success); }
+  .warning { color: var(--el-color-warning); }
+}
+.point-cloud-info {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  font-size: 13px;
+}
+</style>

+ 1 - 1
src/views/tool/build/IconsDialog.vue

@@ -89,7 +89,7 @@ watch(key, (val) => {
 }
 
 .icon-dialog {
-  :deep() {
+  :deep(.el-dialog) {
     .el-dialog {
       border-radius: 8px;
       margin-bottom: 0;

+ 4 - 7
src/views/tool/build/RightPanel.vue

@@ -770,7 +770,7 @@ function tagChange(tagIcon) {
   top: 0;
   padding-top: 3px;
 
-  &:deep() {
+  &:deep(.el-tabs__header) {
     .el-tabs__header {
       margin: 0;
     }
@@ -790,11 +790,8 @@ function tagChange(tagIcon) {
   .el-scrollbar {
     height: 100%;
 
-    &:deep() {
-      .el-scrollbar__view {
-        padding: 30px 20px;
-      }
-
+    :deep(.el-scrollbar__view) {
+      padding: 30px 20px;
     }
   }
 }
@@ -863,7 +860,7 @@ function tagChange(tagIcon) {
     width: 227px;
   }
 
-  :deep() {
+  :deep(.el-icon-time) {
     .el-icon-time {
       display: none;
     }

+ 8 - 1
vite/plugins/auto-import.js

@@ -8,7 +8,14 @@ export default function createAutoImport() {
       'pinia',
       {
         '@/utils/dict': ['useDict'],
-        '@/utils/ruoyi': ['selectDictLabel']
+        '@/utils/ruoyi': ['selectDictLabel'],
+        'element-plus': [
+          'ElMessage',
+          'ElMessageBox',
+          'ElNotification',
+          'ElLoading',
+          'ElIcon'
+        ]
       }
     ],
     dts: false