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

新增参数配置、版本管理页面优化

yawuga 8 месяцев назад
Родитель
Сommit
a9a0ab31d2

+ 216 - 0
src/api/devops/version.js

@@ -0,0 +1,216 @@
+import request from '@/utils/request'
+import { getUploadList as mockGetUploadList } from '@/api/mock/version'
+
+// 获取已安装版本列表
+export function getInstalledVersions(params) {
+  return request({
+    url: '/api/devops/versions/installed',
+    method: 'get',
+    params
+  })
+}
+
+// 上传安装包
+export function uploadPackage(formData) {
+  return request({
+    url: '/api/devops/versions/upload',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}
+
+// 安装软件包
+export function installPackage(params) {
+  return request({
+    url: '/api/devops/versions/install',
+    method: 'post',
+    data: params
+  })
+}
+
+// 卸载软件包
+export function uninstallPackage(params) {
+  return request({
+    url: '/api/devops/versions/uninstall',
+    method: 'post',
+    data: params
+  })
+}
+
+// 获取安装进度
+export function getInstallProgress(taskId) {
+  return request({
+    url: `/api/devops/versions/install/progress/${taskId}`,
+    method: 'get'
+  })
+}
+
+// 获取上传文件列表
+export function getUploadList(params) {
+  return request({
+    url: '/api/devops/versions/uploadList',
+    method: 'get',
+    params
+  })
+}
+
+// Mock API 兜底实现
+const mockApi = {
+  async getInstalledVersions(params = {}) {
+    // 模拟延迟
+    await new Promise(resolve => setTimeout(resolve, 300))
+    
+    const mockData = [
+      {
+        id: 1,
+        name: 'leador-data-analyser',
+        version: '192.168.10.10',
+        updateTime: '2024-01-15 10:30:25'
+      },
+      {
+        id: 2,
+        name: '激光网卡',
+        version: '0.3.1',
+        updateTime: '2024-01-14 16:45:12'
+      },
+      {
+        id: 3,
+        name: 'leador-robot-config-web',
+        version: '0.7.133',
+        updateTime: '2024-01-13 09:15:30'
+      },
+      {
+        id: 4,
+        name: 'ros-kinetic-leador-auto-pursuit-local-planner',
+        version: '0.0.1',
+        updateTime: '2024-01-12 14:20:45'
+      },
+      {
+        id: 5,
+        name: 'ros-kinetic-leador-en-control-msgs',
+        version: '1.0.0',
+        updateTime: '2024-01-11 11:35:20'
+      },
+      {
+        id: 6,
+        name: 'ros-kinetic-leador-ld-sensor-msgs',
+        version: '2.0.1',
+        updateTime: '2024-01-10 08:50:15'
+      }
+    ]
+    
+    // 模拟搜索过滤
+    let filteredData = mockData
+    if (params.search) {
+      const keyword = params.search.toLowerCase()
+      filteredData = mockData.filter(item => 
+        item.name.toLowerCase().includes(keyword) || 
+        item.version.toLowerCase().includes(keyword)
+      )
+    }
+    
+    // 模拟分页
+    const pageNum = params.pageNum || 1
+    const pageSize = params.pageSize || 10
+    const total = filteredData.length
+    const startIndex = (pageNum - 1) * pageSize
+    const endIndex = startIndex + pageSize
+    const list = filteredData.slice(startIndex, endIndex)
+    
+    return Promise.resolve({
+      code: 200,
+      data: {
+        list,
+        total,
+        pageNum,
+        pageSize
+      },
+      message: '获取成功'
+    })
+  },
+
+  async uploadPackage(formData) {
+    await new Promise(resolve => setTimeout(resolve, 1000))
+    return Promise.resolve({
+      code: 200,
+      data: {
+        fileId: 'file_' + Date.now(),
+        fileName: formData.get('file').name,
+        fileSize: formData.get('file').size,
+        url: '/temp/upload/' + formData.get('file').name
+      },
+      message: '上传成功'
+    })
+  },
+
+  async installPackage(params) {
+    await new Promise(resolve => setTimeout(resolve, 2000))
+    return Promise.resolve({
+      code: 200,
+      data: {
+        taskId: 'task_' + Date.now(),
+        status: 'success'
+      },
+      message: '安装成功'
+    })
+  },
+
+  async uninstallPackage(params) {
+    await new Promise(resolve => setTimeout(resolve, 1000))
+    return Promise.resolve({
+      code: 200,
+      message: '卸载成功'
+    })
+  }
+}
+
+// 导出带兜底的API函数
+export const apiWithFallback = {
+  async getInstalledVersions(params) {
+    try {
+      return await getInstalledVersions(params)
+    } catch (error) {
+      console.warn('使用Mock API:', error)
+      return await mockApi.getInstalledVersions(params)
+    }
+  },
+
+  async getUploadList(params) {
+    try {
+      return await getUploadList(params)
+    } catch (error) {
+      console.warn('使用Mock API:', error)
+      return await mockGetUploadList(params)
+    }
+  },
+
+  async uploadPackage(formData) {
+    try {
+      return await uploadPackage(formData)
+    } catch (error) {
+      console.warn('使用Mock API:', error)
+      return await mockApi.uploadPackage(formData)
+    }
+  },
+
+  async installPackage(params) {
+    try {
+      return await installPackage(params)
+    } catch (error) {
+      console.warn('使用Mock API:', error)
+      return await mockApi.installPackage(params)
+    }
+  },
+
+  async uninstallPackage(params) {
+    try {
+      return await uninstallPackage(params)
+    } catch (error) {
+      console.warn('使用Mock API:', error)
+      return await mockApi.uninstallPackage(params)
+    }
+  }
+}

+ 245 - 0
src/api/mock/version.js

@@ -0,0 +1,245 @@
+// 版本管理相关的 Mock API
+
+// 模拟上传文件列表数据
+const mockUploadList = [
+  {
+    id: 'upload_001',
+    fileName: 'leador-navigation-system_v2.1.3.deb',
+    size: 45678912, // 约43.5MB
+    status: '待安装'
+  },
+  {
+    id: 'upload_002', 
+    fileName: 'ros-kinetic-sensor-driver_v1.5.2.tar',
+    size: 23456789, // 约22.4MB
+    status: '待安装'
+  },
+  {
+    id: 'upload_003',
+    fileName: 'lidar-config-tool_v3.0.1.zip',
+    size: 12345678, // 约11.8MB
+    status: '失败'
+  },
+  {
+    id: 'upload_004',
+    fileName: 'vehicle-control-module_v1.8.7.deb',
+    size: 67890123, // 约64.7MB
+    status: '失败'
+  },
+  {
+    id: 'upload_005',
+    fileName: 'map-processor_v2.3.0.tar',
+    size: 34567890, // 约33.0MB
+    status: '待安装'
+  }
+]
+
+// 获取上传文件列表
+export function getUploadList(params = {}) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      // 模拟搜索过滤
+      let filteredData = [...mockUploadList]
+      if (params.search) {
+        const keyword = params.search.toLowerCase()
+        filteredData = mockUploadList.filter(item => 
+          item.fileName.toLowerCase().includes(keyword) || 
+          item.version.toLowerCase().includes(keyword)
+        )
+      }
+
+      // 模拟状态过滤
+      if (params.status && params.status !== 'all') {
+        filteredData = filteredData.filter(item => item.status === params.status)
+      }
+
+      // 模拟分页
+      const pageNum = params.pageNum || 1
+      const pageSize = params.pageSize || 10
+      const total = filteredData.length
+      const startIndex = (pageNum - 1) * pageSize
+      const endIndex = startIndex + pageSize
+      const list = filteredData.slice(startIndex, endIndex)
+
+      resolve({
+        code: 200,
+        data: {
+          list,
+          total,
+          pageNum,
+          pageSize
+        },
+        message: '获取成功'
+      })
+    }, 300) // 模拟网络延迟
+  })
+}
+
+// 上传文件
+export function uploadFile(file) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const newUpload = {
+        id: 'upload_' + Date.now(),
+        fileName: file.name,
+        size: file.size,
+        version: extractVersion(file.name),
+        status: '待安装',
+        progress: 0
+      }
+      
+      // 添加到mock数据中
+      mockUploadList.unshift(newUpload)
+      
+      resolve({
+        code: 200,
+        data: newUpload,
+        message: '上传成功'
+      })
+    }, 1000)
+  })
+}
+
+// 安装文件
+export function installFile(params) {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      const { id } = params
+      const uploadItem = mockUploadList.find(item => item.id === id)
+      
+      if (!uploadItem) {
+        reject({
+          code: 404,
+          message: '文件不存在'
+        })
+        return
+      }
+
+      // 模拟安装成功/失败
+      const isSuccess = Math.random() > 0.2 // 80%成功率
+      
+      if (isSuccess) {
+        uploadItem.status = '已安装'
+        uploadItem.progress = 100
+        resolve({
+          code: 200,
+          data: uploadItem,
+          message: '安装成功'
+        })
+      } else {
+        uploadItem.status = '失败'
+        uploadItem.progress = 0
+        reject({
+          code: 500,
+          message: '安装失败:系统版本不兼容'
+        })
+      }
+    }, 2000)
+  })
+}
+
+// 删除上传文件
+export function deleteUploadFile(params) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const { id } = params
+      const index = mockUploadList.findIndex(item => item.id === id)
+      
+      if (index !== -1) {
+        mockUploadList.splice(index, 1)
+      }
+      
+      resolve({
+        code: 200,
+        message: '删除成功'
+      })
+    }, 500)
+  })
+}
+
+// 批量安装
+export function batchInstall(params) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const { ids } = params
+      const results = []
+      
+      ids.forEach(id => {
+        const uploadItem = mockUploadList.find(item => item.id === id)
+        if (uploadItem && uploadItem.status === '待安装') {
+          // 模拟安装结果
+          const isSuccess = Math.random() > 0.2
+          uploadItem.status = isSuccess ? '已安装' : '失败'
+          uploadItem.progress = isSuccess ? 100 : 0
+          results.push({
+            id,
+            success: isSuccess,
+            message: isSuccess ? '安装成功' : '安装失败'
+          })
+        }
+      })
+      
+      resolve({
+        code: 200,
+        data: results,
+        message: '批量安装完成'
+      })
+    }, 3000)
+  })
+}
+
+// 清空上传列表
+export function clearUploadList() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      // 只清空待安装和失败的文件
+      const activeStatuses = ['安装中']
+      mockUploadList.splice(0, mockUploadList.length, 
+        ...mockUploadList.filter(item => activeStatuses.includes(item.status))
+      )
+      
+      resolve({
+        code: 200,
+        message: '清空成功'
+      })
+    }, 500)
+  })
+}
+
+// 更新文件状态(用于模拟安装进度)
+export function updateFileStatus(params) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const { id, status, progress } = params
+      const uploadItem = mockUploadList.find(item => item.id === id)
+      
+      if (uploadItem) {
+        uploadItem.status = status
+        uploadItem.progress = progress || uploadItem.progress
+      }
+      
+      resolve({
+        code: 200,
+        data: uploadItem,
+        message: '状态更新成功'
+      })
+    }, 100)
+  })
+}
+
+// 辅助函数:从文件名提取版本号
+function extractVersion(filename) {
+  const versionMatch = filename.match(/[vV]?(\d+\.\d+\.\d+)/)
+  return versionMatch ? versionMatch[1] : '-'
+}
+
+// 导出所有API函数
+export default {
+  getUploadList,
+  uploadFile,
+  installFile,
+  deleteUploadFile,
+  batchInstall,
+  clearUploadList,
+  updateFileStatus
+}

+ 334 - 0
src/components/Calibration/SensorCard.vue

@@ -0,0 +1,334 @@
+<template>
+  <el-card class="sensor-card" shadow="hover">
+    <div slot="header" class="card-header">
+      <span class="card-title">{{ title }}</span>
+    </div>
+    
+    <el-form :model="model" :rules="rules" ref="sensorForm" label-width="120px" class="sensor-form">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="X轴偏移" prop="xOffset">
+            <el-input-number
+              v-model="model.xOffset"
+              :precision="3"
+              :step="0.001"
+              :min="-999"
+              :max="999"
+              controls-position="right"
+              placeholder="0.000"
+              class="full-width"
+            />
+            <span class="unit">m</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="Y轴偏移" prop="yOffset">
+            <el-input-number
+              v-model="model.yOffset"
+              :precision="3"
+              :step="0.001"
+              :min="-999"
+              :max="999"
+              controls-position="right"
+              placeholder="0.000"
+              class="full-width"
+            />
+            <span class="unit">m</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="Z轴偏移" prop="zOffset">
+            <el-input-number
+              v-model="model.zOffset"
+              :precision="3"
+              :step="0.001"
+              :min="-999"
+              :max="999"
+              controls-position="right"
+              placeholder="0.000"
+              class="full-width"
+            />
+            <span class="unit">m</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="横滚偏差" prop="roll">
+            <el-input-number
+              v-model="model.roll"
+              :precision="4"
+              :step="0.0001"
+              :min="-3.1416"
+              :max="3.1416"
+              controls-position="right"
+              placeholder="0.0000"
+              class="full-width"
+            />
+            <span class="unit">rad</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="俯仰偏差" prop="pitch">
+            <el-input-number
+              v-model="model.pitch"
+              :precision="4"
+              :step="0.0001"
+              :min="-3.1416"
+              :max="3.1416"
+              controls-position="right"
+              placeholder="0.0000"
+              class="full-width"
+            />
+            <span class="unit">rad</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="航向偏差" prop="yaw">
+            <el-input-number
+              v-model="model.yaw"
+              :precision="4"
+              :step="0.0001"
+              :min="-3.1416"
+              :max="3.1416"
+              controls-position="right"
+              placeholder="0.0000"
+              class="full-width"
+            />
+            <span class="unit">rad</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    
+    <div class="card-actions">
+      <el-button type="primary" size="small" @click="handleSave" :loading="saving">
+        <i class="el-icon-check"></i>
+        保存本卡
+      </el-button>
+    </div>
+  </el-card>
+</template>
+
+<script>
+export default {
+  name: 'SensorCard',
+  
+  props: {
+    title: {
+      type: String,
+      required: true
+    },
+    model: {
+      type: Object,
+      required: true,
+      default: () => ({
+        xOffset: 0,
+        yOffset: 0,
+        zOffset: 0,
+        roll: 0,
+        pitch: 0,
+        yaw: 0
+      })
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  
+  data() {
+    return {
+      saving: false,
+      rules: {
+        xOffset: [
+          { type: 'number', message: '请输入有效数字', trigger: 'blur' }
+        ],
+        yOffset: [
+          { type: 'number', message: '请输入有效数字', trigger: 'blur' }
+        ],
+        zOffset: [
+          { type: 'number', message: '请输入有效数字', trigger: 'blur' }
+        ],
+        roll: [
+          { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+          { 
+            validator: (rule, value, callback) => {
+              if (value !== null && value !== undefined && (value < -3.1416 || value > 3.1416)) {
+                callback(new Error('角度范围应在 -π 到 π 之间'))
+              } else {
+                callback()
+              }
+            }, 
+            trigger: 'blur' 
+          }
+        ],
+        pitch: [
+          { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+          { 
+            validator: (rule, value, callback) => {
+              if (value !== null && value !== undefined && (value < -3.1416 || value > 3.1416)) {
+                callback(new Error('角度范围应在 -π 到 π 之间'))
+              } else {
+                callback()
+              }
+            }, 
+            trigger: 'blur' 
+          }
+        ],
+        yaw: [
+          { type: 'number', message: '请输入有效数字', trigger: 'blur' },
+          { 
+            validator: (rule, value, callback) => {
+              if (value !== null && value !== undefined && (value < -3.1416 || value > 3.1416)) {
+                callback(new Error('角度范围应在 -π 到 π 之间'))
+              } else {
+                callback()
+              }
+            }, 
+            trigger: 'blur' 
+          }
+        ]
+      }
+    }
+  },
+  
+  methods: {
+    async handleSave() {
+      try {
+        await this.$refs.sensorForm.validate()
+        this.saving = true
+        this.$emit('save', { ...this.model })
+        
+        // 模拟保存延迟
+        setTimeout(() => {
+          this.saving = false
+          this.$message.success(`${this.title}参数保存成功`)
+        }, 500)
+      } catch (error) {
+        this.$message.error('请检查输入数据')
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sensor-card {
+  margin-bottom: 20px;
+  border-radius: 12px;
+  transition: all 0.3s ease;
+  
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
+  }
+  
+  .card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    
+    .card-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: var(--color-text-primary, #303133);
+    }
+  }
+  
+  .sensor-form {
+    ::v-deep .el-form-item {
+      margin-bottom: 20px;
+      
+      .el-form-item__label {
+        font-weight: 500;
+        color: var(--color-text-secondary, #606266);
+      }
+      
+      .el-form-item__content {
+        display: flex;
+        align-items: center;
+        
+        .el-input-number {
+          &.full-width {
+            width: 100%;
+            
+            .el-input__inner {
+              height: 36px;
+              border-radius: 8px;
+              transition: all 0.2s ease;
+              
+              &:focus {
+                border-color: var(--color-primary, #409eff);
+                box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
+              }
+            }
+          }
+        }
+        
+        .unit {
+          margin-left: 8px;
+          color: var(--color-text-secondary, #909399);
+          font-size: 14px;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+  
+  .card-actions {
+    text-align: center;
+    padding-top: 16px;
+    border-top: 1px solid var(--color-border-lighter, #f0f0f0);
+    
+    .el-button {
+      border-radius: 8px;
+      padding: 8px 20px;
+      font-weight: 500;
+      
+      &.el-button--primary {
+        background: linear-gradient(135deg, #409eff, #3a8ee6);
+        border: none;
+        
+        &:hover {
+          background: linear-gradient(135deg, #3a8ee6, #337ecc);
+          transform: translateY(-1px);
+          box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+        }
+      }
+    }
+  }
+}
+
+// 暗色主题适配
+html.dark {
+  .sensor-card {
+    ::v-deep .el-card {
+      background: var(--color-bg-tertiary);
+      border-color: var(--color-border-secondary);
+    }
+    
+    .card-title {
+      color: var(--color-text-primary);
+    }
+    
+    .sensor-form {
+      ::v-deep .el-form-item__label {
+        color: var(--color-text-secondary);
+      }
+      
+      .unit {
+        color: var(--color-text-tertiary);
+      }
+    }
+    
+    .card-actions {
+      border-color: var(--color-border-secondary);
+    }
+  }
+}
+</style>

+ 163 - 7
src/components/XtParamItem/index.vue

@@ -8,8 +8,10 @@
           <i :class="value ? 'el-icon-check' : 'el-icon-close'" :style="{ color: value ? '#67C23A' : '#F56C6C' }"></i>
           {{ value ? '已启用' : '已禁用' }}
         </span>
-        <span v-else-if="type === 'textarea'" class="textarea-value" :class="{ 'monospace': type === 'textarea' }">{{ value || placeholder || '未配置' }}</span>
-        <span v-else class="text-value">{{ value || placeholder || '未配置' }}</span>
+        <span v-else-if="type === 'textarea'" class="textarea-value" :class="{ 'monospace': type === 'textarea' }">{{ displayValue }}</span>
+        <span v-else-if="type === 'select'" class="text-value">{{ displayValue }}</span>
+        <span v-else-if="type === 'number'" class="text-value">{{ displayValue }}</span>
+        <span v-else class="text-value">{{ displayValue }}</span>
       </div>
       <div class="param-actions">
         <!-- 复制按钮 -->
@@ -81,6 +83,30 @@
             </el-button>
           </el-popover>
         </div>
+        <!-- 数字输入类型 -->
+        <el-input
+          v-else-if="type === 'number'"
+          v-model="editValue"
+          :placeholder="placeholder"
+          :disabled="saving"
+          ref="editInput"
+          @input="handleNumberInput"
+        />
+        <!-- 下拉选择类型 -->
+        <el-select
+          v-else-if="type === 'select'"
+          v-model="editValue"
+          :placeholder="placeholder"
+          :disabled="saving"
+          ref="editInput"
+        >
+          <el-option
+            v-for="option in options"
+            :key="option.value"
+            :label="option.label"
+            :value="option.value"
+          />
+        </el-select>
         <!-- 其他输入类型 -->
         <el-input
           v-else
@@ -140,7 +166,7 @@ export default {
     type: {
       type: String,
       default: 'text',
-      validator: value => ['text', 'url', 'ip', 'switch', 'textarea'].includes(value)
+      validator: value => ['text', 'url', 'ip', 'switch', 'textarea', 'number', 'select'].includes(value)
     },
     placeholder: {
       type: String,
@@ -153,6 +179,32 @@ export default {
     searchKeyword: {
       type: String,
       default: ''
+    },
+    // 数字类型相关属性
+    unit: {
+      type: String,
+      default: ''
+    },
+    min: {
+      type: Number,
+      default: undefined
+    },
+    max: {
+      type: Number,
+      default: undefined
+    },
+    step: {
+      type: Number,
+      default: 0.01
+    },
+    precision: {
+      type: Number,
+      default: 2
+    },
+    // 选择类型相关属性
+    options: {
+      type: Array,
+      default: () => []
     }
   },
 
@@ -178,7 +230,20 @@ export default {
   computed: {
     // 是否显示复制按钮
     showCopyButton() {
-      return ['ip', 'url', 'text'].includes(this.type) && this.value && this.value.toString().trim()
+      return ['ip', 'url', 'text', 'number', 'textarea', 'select'].includes(this.type) && this.value && this.value.toString().trim()
+    },
+
+    // 显示值
+    displayValue() {
+      if (this.type === 'switch') {
+        return this.value ? '已启用' : '已禁用'
+      } else if (this.type === 'select') {
+        const option = this.options.find(opt => opt.value === this.value)
+        return option ? option.label : this.value
+      } else if (this.type === 'number' && this.unit) {
+        return this.value !== null && this.value !== undefined ? `${this.value} ${this.unit}` : (this.placeholder || '未设置')
+      }
+      return this.value || this.placeholder || '未设置'
     },
 
     // 高亮标签
@@ -226,6 +291,19 @@ export default {
     async handleSave() {
       if (this.saving) return
 
+      // 验证数字输入
+      if (this.type === 'number') {
+        const validation = this.validateNumber(this.editValue)
+        if (!validation.valid) {
+          this.hasError = true
+          this.errorMessage = validation.message
+          return
+        }
+        
+        // 转换为数字类型
+        this.editValue = this.editValue === '' ? null : parseFloat(this.editValue)
+      }
+
       // 校验
       const validation = this.validateValue(this.editValue)
       if (!validation.valid) {
@@ -270,12 +348,14 @@ export default {
     // 复制功能
     async handleCopy() {
       try {
-        await navigator.clipboard.writeText(this.value.toString())
+        const copyValue = this.type === 'select' ? this.displayValue : this.value.toString()
+        await navigator.clipboard.writeText(copyValue)
         this.$message.success('已复制')
       } catch (error) {
         // Fallback for older browsers
+        const copyValue = this.type === 'select' ? this.displayValue : this.value.toString()
         const textArea = document.createElement('textarea')
-        textArea.value = this.value.toString()
+        textArea.value = copyValue
         document.body.appendChild(textArea)
         textArea.select()
         document.execCommand('copy')
@@ -298,6 +378,56 @@ export default {
       }
     },
 
+    // 数字输入处理
+    handleNumberInput(value) {
+      if (this.type !== 'number') return
+      
+      // 允许输入负号、小数点和数字
+      const numericValue = value.replace(/[^-0-9.]/g, '')
+      
+      // 确保只有一个负号且在开头
+      let cleanValue = numericValue.replace(/-/g, '')
+      if (numericValue.indexOf('-') === 0) {
+        cleanValue = '-' + cleanValue
+      }
+      
+      // 确保只有一个小数点
+      const parts = cleanValue.split('.')
+      if (parts.length > 2) {
+        cleanValue = parts[0] + '.' + parts.slice(1).join('')
+      }
+      
+      this.editValue = cleanValue
+    },
+
+    // 验证数字值
+    validateNumber(value) {
+      if (this.type !== 'number') return { valid: true }
+      
+      // 空值检查
+      if (value === '' || value === null || value === undefined) {
+        return { valid: true } // 允许空值
+      }
+      
+      const numValue = parseFloat(value)
+      
+      // 检查是否为有效数字
+      if (isNaN(numValue)) {
+        return { valid: false, message: '请输入有效的数字' }
+      }
+      
+      // 检查范围
+      if (this.min !== undefined && numValue < this.min) {
+        return { valid: false, message: `值不能小于 ${this.min}` }
+      }
+      
+      if (this.max !== undefined && numValue > this.max) {
+        return { valid: false, message: `值不能大于 ${this.max}` }
+      }
+      
+      return { valid: true }
+    },
+
     validateValue(value) {
       if (this.type === 'switch') {
         return { valid: true }
@@ -315,6 +445,8 @@ export default {
           return this.validateURL(value)
         case 'text':
         case 'textarea':
+        case 'number':
+        case 'select':
         default:
           return { valid: true }
       }
@@ -419,7 +551,8 @@ export default {
     min-width: 0;
 
     .el-input,
-    .el-switch {
+    .el-switch,
+    .el-select {
       width: 100%;
     }
 
@@ -451,6 +584,21 @@ export default {
       }
     }
 
+    .el-select {
+      ::v-deep .el-input__inner {
+        height: 36px;
+        border-radius: var(--radius-base);
+        border: 1px solid var(--color-border-primary);
+        background: var(--color-bg-card);
+        transition: all var(--duration-200) var(--ease-out);
+
+        &:focus {
+          border-color: var(--color-primary);
+          box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+        }
+      }
+    }
+
     ::v-deep .el-textarea {
       .el-textarea__inner {
         border-radius: var(--radius-base);
@@ -626,6 +774,14 @@ html.dark {
         }
       }
 
+      .el-select {
+        ::v-deep .el-input__inner {
+          background: var(--color-bg-tertiary);
+          border-color: var(--color-border-tertiary);
+          color: var(--color-text-primary);
+        }
+      }
+
       ::v-deep .el-textarea {
         .el-textarea__inner {
           background: var(--color-bg-tertiary);

+ 1171 - 103
src/views/config/calibration/index.vue

@@ -1,132 +1,1200 @@
 <template>
-	<div class="app-container">
-		<div class="hearder">
-			<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
-				<!-- <el-form-item label="参数类型" prop="mapName">
-					<el-input v-model="queryParams.paramName" placeholder="请输入地图名称" clearable />
-				</el-form-item> -->
-				<el-form-item label="参数名称" prop="mapName">
-					<el-input v-model="queryParams.paramType" placeholder="请输入参数名称" clearable />
-				</el-form-item>
-				<el-form-item>
-					<el-button type="primary" icon="el-icon-search" size="mini" @click="">搜索</el-button>
-					<el-button icon="el-icon-refresh" size="mini" @click="">重置</el-button>
-				</el-form-item>
-			</el-form>
+  <div class="calibration-config-simple">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="header-main">
+          <h1 class="page-title">标定参数</h1>
+          <p class="page-desc">配置车辆和传感器标定参数,逐项编辑保存</p>
+        </div>
+      </div>
+      
+      <!-- 搜索框 -->
+      <div class="search-section">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索标定参数..."
+          clearable
+          prefix-icon="el-icon-search"
+          class="search-input"
+        />
+      </div>
+    </div>
+
+    <!-- 主内容区域 -->
+    <div class="page-content">
+      <div class="xt-calib">
+        <!-- 顶部全宽示意图卡片:左右布局 -->
+        <el-card class="xt-card diagram-hero" shadow="never">
+          <div class="hero-grid">
+            <div class="hero-left">
+              <div class="diagram-box">
+                <img class="diagram-img" :src="diagramUrl" alt="车辆坐标系示意" />
+              </div>
+              <div class="diagram-caption">车辆坐标系示意</div>
+            </div>
+            <div class="hero-right">
+              <div class="hero-right-inner">
+                <div class="hero-title">坐标系说明</div>
+                <ul class="hero-list">
+                  <li><b>X 轴:</b>车辆前进方向为正</li>
+                  <li><b>Y 轴:</b>车辆左侧方向为正</li>
+                  <li><b>Z 轴:</b>垂直向上方向为正</li>
+                  <li><b>Roll:</b>绕 X 轴旋转角度</li>
+                  <li><b>Pitch:</b>绕 Y 轴旋转角度</li>
+                  <li><b>Yaw:</b>绕 Z 轴旋转角度</li>
+                </ul>
+                <el-alert
+                  class="hero-tip"
+                  type="info"
+                  :closable="false"
+                  title="请确保传感器安装位置相对于车辆坐标系的准确性"
+                  show-icon
+                />
+              </div>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 参数卡片区:两列网格 -->
+        <div class="cards-grid">
+          <!-- 车身参数卡片 -->
+          <el-card class="xt-card calib-card" shadow="never" v-show="hasVisibleItems('vehicle')">
+            <div slot="header" class="card-header">
+              <h2 class="card-title">车身参数</h2>
+              <p class="card-desc">车辆底盘类型、尺寸和轮廓配置</p>
+            </div>
+            <XtParamItem
+              v-for="item in vehicleConfigItems"
+              :key="item.key"
+              v-show="isItemVisible(item)"
+              :label="item.label"
+              :value="config[item.key]"
+              :type="item.type"
+              :unit="item.unit"
+              :placeholder="item.placeholder"
+              :options="item.options"
+              :precision="item.precision"
+              :step="item.step"
+              :min="item.min"
+              :max="item.max"
+              :search-keyword="searchKeyword"
+              @save="(value) => handleParamSave(item.key, value)"
+              @cancel="handleParamCancel"
+            />
+            <div v-if="!hasVisibleItems('vehicle')" class="empty-state">
+              <i class="el-icon-search"></i>
+              <p>无匹配的配置参数</p>
+            </div>
+          </el-card>
+
+          <!-- 挂车参数卡片 -->
+          <el-card class="xt-card calib-card" shadow="never" v-show="hasVisibleItems('trailer')">
+            <div slot="header" class="card-header">
+              <h2 class="card-title">挂车参数</h2>
+              <p class="card-desc">挂车类型、数量和尺寸配置</p>
+            </div>
+            <XtParamItem
+              v-for="item in trailerConfigItems"
+              :key="item.key"
+              v-show="isItemVisible(item)"
+              :label="item.label"
+              :value="config[item.key]"
+              :type="item.type"
+              :unit="item.unit"
+              :placeholder="item.placeholder"
+              :options="item.options"
+              :precision="item.precision"
+              :step="item.step"
+              :min="item.min"
+              :max="item.max"
+              :search-keyword="searchKeyword"
+              @save="(value) => handleParamSave(item.key, value)"
+              @cancel="handleParamCancel"
+            />
+            <div v-if="!hasVisibleItems('trailer')" class="empty-state">
+              <i class="el-icon-search"></i>
+              <p>无匹配的配置参数</p>
+            </div>
+          </el-card>
+
+          <!-- 激光雷达参数卡片 -->
+          <el-card class="xt-card calib-card" shadow="never" v-show="hasVisibleItems('lidar')">
+            <div slot="header" class="card-header">
+              <h2 class="card-title">激光雷达</h2>
+              <p class="card-desc">激光雷达传感器位置和姿态标定</p>
+            </div>
+            <XtParamItem
+              v-for="item in lidarConfigItems"
+              :key="item.key"
+              v-show="isItemVisible(item)"
+              :label="item.label"
+              :value="config[item.key]"
+              :type="item.type"
+              :unit="item.unit"
+              :placeholder="item.placeholder"
+              :precision="item.precision"
+              :step="item.step"
+              :min="item.min"
+              :max="item.max"
+              :search-keyword="searchKeyword"
+              @save="(value) => handleParamSave(item.key, value)"
+              @cancel="handleParamCancel"
+            />
+            <div v-if="!hasVisibleItems('lidar')" class="empty-state">
+              <i class="el-icon-search"></i>
+              <p>无匹配的配置参数</p>
 		</div>
-		<el-table :data="data" border style="width: 100%">
-			<el-table-column type="index" width="50" label="序号" align="center"></el-table-column>
-			<el-table-column prop="paramName" label="参数" align="center"></el-table-column>
-			<el-table-column prop="value" label="当前值" align="center">
-				<template slot-scope="scope">
-					<div class="edit-input">
-						<el-input v-model="scope.row.value" placeholder="" :disabled="editingRow !== scope.row"
-							style="width: 60%;" />
-						<el-button type="success" icon="el-icon-check" size="mini" style="margin-left: 10px; padding: 10px 6px;"
-							v-if="editingRow === scope.row" @click="confirmEdit(scope.row)"></el-button>
-						<el-button type="danger" icon="el-icon-close" size="mini" style="padding: 10px 6px;"
-							v-if="editingRow === scope.row" @click="cancelEdit"></el-button>
+          </el-card>
+
+          <!-- IMU参数卡片 -->
+          <el-card class="xt-card calib-card" shadow="never" v-show="hasVisibleItems('imu')">
+            <div slot="header" class="card-header">
+              <h2 class="card-title">IMU</h2>
+              <p class="card-desc">惯性测量单元位置和姿态标定</p>
+            </div>
+            <XtParamItem
+              v-for="item in imuConfigItems"
+              :key="item.key"
+              v-show="isItemVisible(item)"
+              :label="item.label"
+              :value="config[item.key]"
+              :type="item.type"
+              :unit="item.unit"
+              :placeholder="item.placeholder"
+              :precision="item.precision"
+              :step="item.step"
+              :min="item.min"
+              :max="item.max"
+              :search-keyword="searchKeyword"
+              @save="(value) => handleParamSave(item.key, value)"
+              @cancel="handleParamCancel"
+            />
+            <div v-if="!hasVisibleItems('imu')" class="empty-state">
+              <i class="el-icon-search"></i>
+              <p>无匹配的配置参数</p>
 					</div>
-				</template>
-			</el-table-column>
-			<el-table-column prop="paramType" label="类型" align="center" :filters="filters"
-				:filter-method="filterType"></el-table-column>
-			<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="80">
-				<template slot-scope="scope">
-					<el-button size="mini" type="text" icon="el-icon-edit-outline" @click="editParams(scope.row)">编辑</el-button>
-				</template>
-			</el-table-column>
-		</el-table>
-		<div class="card-images" style="margin-top: 30px;">
-			<el-card class="box-card" shadow="hover">
-				<div slot="header" class="clearfix">
-					<span>坐标示意</span>
+          </el-card>
+
+          <!-- GPS参数卡片 -->
+          <el-card class="xt-card calib-card" shadow="never" v-show="hasVisibleItems('gps')">
+            <div slot="header" class="card-header">
+              <h2 class="card-title">GPS</h2>
+              <p class="card-desc">全球定位系统位置和姿态标定</p>
+            </div>
+            <XtParamItem
+              v-for="item in gpsConfigItems"
+              :key="item.key"
+              v-show="isItemVisible(item)"
+              :label="item.label"
+              :value="config[item.key]"
+              :type="item.type"
+              :unit="item.unit"
+              :placeholder="item.placeholder"
+              :precision="item.precision"
+              :step="item.step"
+              :min="item.min"
+              :max="item.max"
+              :search-keyword="searchKeyword"
+              @save="(value) => handleParamSave(item.key, value)"
+              @cancel="handleParamCancel"
+            />
+            <div v-if="!hasVisibleItems('gps')" class="empty-state">
+              <i class="el-icon-search"></i>
+              <p>无匹配的配置参数</p>
 				</div>
-				<img src="@/assets/images/coordinate.png" class="calib__image" />
 			</el-card>
+        </div>
+      </div>
 		</div>
 	</div>
 </template>
 
 <script>
+import XtParamItem from '@/components/XtParamItem'
+
+// 尝试导入真实 API,失败则使用 Mock
+let calibrationApi
+try {
+  calibrationApi = require('@/api/config/calibration')
+} catch (error) {
+  // Fallback 到 Mock API
+  calibrationApi = {
+    getCalibrationParams: () => Promise.resolve({ code: 200, data: {} }),
+    updateParam: ({ key, value }) => Promise.resolve({ code: 200, message: '参数保存成功' })
+  }
+}
+
 export default {
+  name: 'CalibrationConfig',
+  
+  components: {
+    XtParamItem
+  },
+  
 	data() {
 		return {
-			queryParams: {
-				pageNum: 1,
-				pageSize: 10,
-				paramName: '',
-				paramType: '',
-			},
-			filters: [{ text: '车身参数', value: '车身参数' }, { text: '挂车参数', value: '挂车参数' }, { text: '激光雷达', value: '激光雷达' }, { text: 'IMU', value: 'IMU' }, { text: 'GPS', value: 'GPS' }],
-			editingRow: null, // 当前正在编辑的行
-			data: [
-				{ id: '', paramName: '底盘类型(阿克曼:20 差速:10)', paramType: '车身参数', value: 0.42 },
-				{ id: '', paramName: '前后轴距(m)', paramType: '车身参数', value: 0.435 },
-				{ id: '', paramName: '左右轮距(m)', paramType: '车身参数', value: 2.0 },
-				{ id: '', paramName: '挂车类型(转向:1 非转向:2)', paramType: '挂车参数', value: '' },
-				{ id: '', paramName: '挂车数量', paramType: '挂车参数', value: '' },
-				{ id: '', paramName: '显示轮廓', paramType: '挂车参数', value: '' },
-				{ id: '', paramName: 'X轴偏移(m)', paramType: '激光雷达', value: 0.0 },
-				{ id: '', paramName: 'Y轴偏移(m)', paramType: '激光雷达', value: 0.0 },
-				{ id: '', paramName: 'Z轴偏移(m)', paramType: '激光雷达', value: 0.0 },
-				{ id: '', paramName: 'X轴偏移(m)', paramType: 'IMU', value: 0.0 },
-				{ id: '', paramName: 'Y轴偏移(m)', paramType: 'IMU', value: 0.0 },
-				{ id: '', paramName: 'Z轴偏移(m)', paramType: 'IMU', value: 0.0 },
-				{ id: '', paramName: 'X轴偏移(m)', paramType: 'GPS', value: 0.0 },
-				{ id: '', paramName: 'Y轴偏移(m)', paramType: 'GPS', value: 0.0 },
-				{ id: '', paramName: 'Z轴偏移(m)', paramType: 'GPS', value: 0.0 }
-			]
-		}
-	},
+      // 加载状态
+      loading: false,
+      
+      // 搜索关键词
+      searchKeyword: '',
+      
+      // 屏幕尺寸
+      isLargeScreen: false,
+      
+      // 坐标系示意图
+      diagramUrl: require('@/assets/images/coordinate.png'),
+      
+      // 配置数据
+      config: {
+        // 车身参数
+        chassisType: 20, // 阿克曼:20, 差速:10
+        wheelBase: 2.0, // 前后轴距(m)
+        tread: 1.5, // 左右轮距(m)
+        length: 4.5, // 车身长度(m)
+        width: 1.8, // 车身宽度(m)
+        rearOverhang: 0.8, // 后端距(m)
+        safetyContour: '', // 安全轮廓
+        minTurningRadius: 5.0, // 最小转弯半径(m)
+        
+        // 挂车参数
+        trailerType: 1, // 转向:1, 非转向:2
+        trailerCount: 0, // 挂车数量
+        showTrailerContour: '', // 显示轮廓
+        tractorToFrontHitch: 0, // 牵引车后挂距(m)
+        trailerFrontHitch: 0, // 挂车前挂距(m)
+        trailerWheelBase: 0, // 挂车轴距(m)
+        trailerRearHitch: 0, // 挂车后挂距(m)
+        trailerLength: 0, // 挂车长度(m)
+        trailerWidth: 0, // 挂车宽度(m)
+        trailerRearOverhang: 0, // 挂车后端距(m)
+        
+        // 激光雷达
+        lidarXOffset: 0,
+        lidarYOffset: 0,
+        lidarZOffset: 0,
+        lidarRoll: 0,
+        lidarPitch: 0,
+        lidarYaw: 0,
+        
+        // IMU
+        imuXOffset: 0,
+        imuYOffset: 0,
+        imuZOffset: 0,
+        imuRoll: 0,
+        imuPitch: 0,
+        imuYaw: 0,
+        
+        // GPS
+        gpsXOffset: 0,
+        gpsYOffset: 0,
+        gpsZOffset: 0,
+        gpsRoll: 0,
+        gpsPitch: 0,
+        gpsYaw: 0
+      },
+      
+      // 车身参数配置项
+      vehicleConfigItems: [
+        {
+          key: 'chassisType',
+          label: '底盘类型',
+          type: 'select',
+          placeholder: '请选择底盘类型',
+          options: [
+            { label: '阿克曼', value: 20 },
+            { label: '差速', value: 10 }
+          ]
+        },
+        {
+          key: 'wheelBase',
+          label: '前后轴距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'tread',
+          label: '左右轮距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'length',
+          label: '车身长度',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'width',
+          label: '车身宽度',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'rearOverhang',
+          label: '后端距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'minTurningRadius',
+          label: '最小转弯半径',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'safetyContour',
+          label: '安全轮廓',
+          type: 'textarea',
+          placeholder: '[[x1,y1],[x2,y2],...]'
+        }
+      ],
+      
+      // 挂车参数配置项
+      trailerConfigItems: [
+        {
+          key: 'trailerType',
+          label: '挂车类型',
+          type: 'select',
+          placeholder: '请选择挂车类型',
+          options: [
+            { label: '转向', value: 1 },
+            { label: '非转向', value: 2 }
+          ]
+        },
+        {
+          key: 'trailerCount',
+          label: '挂车数量',
+          type: 'number',
+          unit: '台',
+          placeholder: '0',
+          step: 1,
+          min: 0
+        },
+        {
+          key: 'showTrailerContour',
+          label: '显示轮廓',
+          type: 'text',
+          placeholder: '请输入轮廓显示设置'
+        },
+        {
+          key: 'tractorToFrontHitch',
+          label: '牵引车后挂距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'trailerFrontHitch',
+          label: '挂车前挂距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'trailerWheelBase',
+          label: '挂车轴距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'trailerRearHitch',
+          label: '挂车后挂距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'trailerLength',
+          label: '挂车长度',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'trailerWidth',
+          label: '挂车宽度',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'trailerRearOverhang',
+          label: '挂车后端距',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.00',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        }
+      ],
+      
+      // 激光雷达配置项
+      lidarConfigItems: [
+        {
+          key: 'lidarXOffset',
+          label: 'X轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'lidarYOffset',
+          label: 'Y轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'lidarZOffset',
+          label: 'Z轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'lidarRoll',
+          label: '横滚偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        },
+        {
+          key: 'lidarPitch',
+          label: '俯仰偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        },
+        {
+          key: 'lidarYaw',
+          label: '航向偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        }
+      ],
+      
+      // IMU配置项
+      imuConfigItems: [
+        {
+          key: 'imuXOffset',
+          label: 'X轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'imuYOffset',
+          label: 'Y轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'imuZOffset',
+          label: 'Z轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'imuRoll',
+          label: '横滚偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        },
+        {
+          key: 'imuPitch',
+          label: '俯仰偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        },
+        {
+          key: 'imuYaw',
+          label: '航向偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        }
+      ],
+      
+      // GPS配置项
+      gpsConfigItems: [
+        {
+          key: 'gpsXOffset',
+          label: 'X轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'gpsYOffset',
+          label: 'Y轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'gpsZOffset',
+          label: 'Z轴偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.000',
+          precision: 3,
+          step: 0.001,
+          min: -999,
+          max: 999
+        },
+        {
+          key: 'gpsRoll',
+          label: '横滚偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        },
+        {
+          key: 'gpsPitch',
+          label: '俯仰偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        },
+        {
+          key: 'gpsYaw',
+          label: '航向偏差',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0000',
+          precision: 4,
+          step: 0.0001,
+          min: -3.1416,
+          max: 3.1416
+        }
+      ]
+    }
+  },
+  
+    async created() {
+    await this.loadConfig()
+    this.calculateScreenSize()
+    window.addEventListener('resize', this.calculateScreenSize)
+  },
+  
+  beforeDestroy() {
+    window.removeEventListener('resize', this.calculateScreenSize)
+  },
+  
 	methods: {
-		// 进入编辑模式
-		editParams(row) {
-			this.editingRow = row; // 设置为当前行
-		},
-		// 确认编辑
-		confirmEdit(row) {
-			// 确认后可以做一些保存操作(如果需要)
-			// todo: 提交修改
-
-			this.editingRow = null; // 编辑完后恢复禁用
-		},
-		// 取消编辑
-		cancelEdit() {
-			this.editingRow = null; // 取消编辑恢复禁用
-		},
-		filterType(value, row) {
-			return row.paramType === value;
-		},
+    // 加载配置
+    async loadConfig() {
+      this.loading = true
+      try {
+        const response = await calibrationApi.getCalibrationParams()
+        if (response.code === 200 && response.data) {
+          this.config = { ...this.config, ...response.data }
+        }
+      } catch (error) {
+        console.warn('加载标定参数失败,使用默认值:', error)
+        this.$message.warning('加载配置失败,使用默认参数')
+      } finally {
+        this.loading = false
+      }
+    },
+
+    // 保存单个参数
+    async handleParamSave(key, value) {
+      try {
+        const response = await calibrationApi.updateParam({ key, value })
+        if (response.code === 200) {
+          this.config[key] = value
+          // 不显示保存成功消息,由组件内部显示
+        } else {
+          throw new Error(response.message || '保存失败')
+        }
+      } catch (error) {
+        this.$message.error('保存失败: ' + error.message)
+        throw error // 重新抛出错误,让组件显示错误状态
+      }
+    },
+
+    // 取消参数编辑
+    handleParamCancel() {
+      // 可以在这里添加取消编辑的逻辑
+      console.log('参数编辑已取消')
+    },
+
+    // 检查项目是否可见(搜索过滤)
+    isItemVisible(item) {
+      if (!this.searchKeyword.trim()) {
+        return true
+      }
+      const keyword = this.searchKeyword.toLowerCase().trim()
+      return item.label.toLowerCase().includes(keyword)
+    },
+
+    // 检查分组是否有可见项目
+    hasVisibleItems(groupType) {
+      let items = []
+      switch (groupType) {
+        case 'vehicle':
+          items = this.vehicleConfigItems
+          break
+        case 'trailer':
+          items = this.trailerConfigItems
+          break
+        case 'lidar':
+          items = this.lidarConfigItems
+          break
+        case 'imu':
+          items = this.imuConfigItems
+          break
+        case 'gps':
+          items = this.gpsConfigItems
+          break
+        default:
+          return false
+      }
+      
+      return items.some(item => this.isItemVisible(item))
+    },
+
+    // 计算屏幕尺寸
+    calculateScreenSize() {
+      this.isLargeScreen = window.innerWidth >= 1440
+    },
+
 	}
 }
 </script>
 
-<style scoped>
-::v-deep .edit-input .el-input.is-disabled .el-input__inner {
-	background-color: #ffffff;
-	border-color: #E4E7ED;
-	color: #616161;
-	cursor: not-allowed;
-	border: none;
+<style lang="scss" scoped>
+.calibration-config-simple {
+  min-height: 100vh;
+  background: var(--color-bg-secondary);
+  padding-bottom: var(--spacing-6);
+
+  .page-header {
+    background: var(--color-bg-card);
+    border-bottom: 1px solid var(--color-border-primary);
+    padding: var(--spacing-6);
+    margin-bottom: var(--spacing-6);
+    box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+
+    .header-content {
+      max-width: 1200px;
+      margin: 0 auto;
+
+      .header-main {
+        .page-title {
+          color: var(--color-text-primary);
+          font-size: var(--font-size-3xl);
+          font-weight: var(--font-weight-bold);
+          margin: 0 0 var(--spacing-2) 0;
+          line-height: var(--line-height-tight);
+        }
+
+        .page-desc {
+          color: var(--color-text-secondary);
+          font-size: var(--font-size-base);
+          margin: 0 0 var(--spacing-4) 0;
+          line-height: var(--line-height-relaxed);
+        }
+      }
+    }
+
+    .search-section {
+      max-width: 1200px;
+      margin: 0 auto;
+
+      .search-input {
+        max-width: 400px;
+
+        ::v-deep .el-input__inner {
+          border-radius: var(--radius-lg);
+          border: 1px solid var(--color-border-primary);
+          background: var(--color-bg-card);
+          transition: all var(--duration-200) var(--ease-out);
+
+          &:focus {
+            border-color: var(--color-primary);
+            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+          }
+        }
+
+        ::v-deep .el-input__prefix {
+          color: var(--color-text-tertiary);
+        }
+      }
+    }
+  }
+
+  .page-content {
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 var(--spacing-6);
+  }
 }
 
-::v-deep .edit-input .el-input__inner {
-	text-align: center;
+/* 新布局样式 */
+.xt-calib {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
 }
 
-.calib__image {
-	width: 40%;
-	height: auto;
+/* 顶部全宽卡片 */
+.diagram-hero {
+  width: 100%;
+  border-radius: 14px;
+  box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+  transition: all var(--duration-200) var(--ease-out);
+  padding: 16px 20px !important; /* 适中的卡片内边距 */
+  
+  &:hover {
+    box-shadow: 0 8px 24px rgba(2, 6, 23, 0.08);
+  }
+}
+
+.hero-grid {
+  display: grid;
+  grid-template-columns: 7fr 5fr;
+  gap: 16px;
+  align-items: start; /* 顶部对齐更稳 */
+}
+
+/* 固定图片展示区域的高度(不改卡片其它高度) */
+.diagram-box {
+  height: 320px; /* 根据你卡片可用空间设定,和当前高度匹配 */
+  width: 100%;
+  overflow: visible; /* 不裁剪 */
+}
+
+/* 图片在盒子内按比例完整缩放 */
+.diagram-img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain; /* 关键:完整显示,不裁切 */
+  display: block;
 }
 
-::v-deep .card-images .el-card__body,
-.el-main {
+.diagram-caption {
+  margin-top: 6px;
 	text-align: center;
+  font-size: 12px;
+  color: #6b7280;
+}
+
+/* 右侧说明区域 */
+.hero-right-inner {
+  padding: 0;
+  height: 320px; /* 与左侧图片容器高度保持一致 */
+  display: flex;
+  flex-direction: column;
+  justify-content: center; /* 垂直居中对齐 */
+}
+
+.hero-title {
+  font-weight: 600;
+  font-size: 15px;
+  margin-bottom: 12px;
+  color: var(--color-text-primary);
+}
+
+.hero-list {
+  margin: 0 0 12px 0;
+  padding-left: 16px;
+  line-height: 1.5;
+  font-size: 13px;
+  list-style: none;
+  
+  li {
+    margin-bottom: 6px;
+    color: var(--color-text-secondary);
+    position: relative;
+    
+    &:before {
+      content: '•';
+      color: var(--color-primary);
+      font-weight: bold;
+      position: absolute;
+      left: -12px;
+    }
+    
+    b {
+      color: #111827;
+      font-weight: 600;
+    }
+  }
+}
+
+.hero-tip {
+  margin-top: 0;
+}
+
+/* 参数卡片两列网格(小屏一列) */
+.cards-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+/* 保持与通用卡片风格一致 */
+.xt-card {
+  border-radius: 14px;
+  box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+  transition: all var(--duration-200) var(--ease-out);
+  animation: slideInUp 0.3s ease-out;
+  
+  &:hover {
+    box-shadow: 0 8px 24px rgba(2, 6, 23, 0.08);
+  }
+  
+  ::v-deep .el-card__header {
+    padding: 20px 20px 16px 20px;
+    border-bottom: 1px solid var(--color-border-secondary);
+  }
+  
+  .card-header {
+    .card-title {
+      font-size: var(--font-size-lg);
+      font-weight: var(--font-weight-semibold);
+      color: var(--color-text-primary);
+      margin: 0 0 4px 0;
+      line-height: var(--line-height-tight);
+    }
+    
+    .card-desc {
+      font-size: var(--font-size-sm);
+      color: var(--color-text-tertiary);
+      margin: 0;
+      line-height: var(--line-height-relaxed);
+      opacity: 0.8;
+    }
+  }
+  
+  ::v-deep .el-card__body {
+    padding: 16px 20px 20px 20px;
+  }
+}
+
+
+/* 空状态样式 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 120px;
+  color: var(--color-text-quaternary);
+  background: rgba(0, 0, 0, 0.02);
+  border-radius: var(--radius-base);
+  border: 1px dashed var(--color-border-secondary);
+
+  i {
+    font-size: var(--font-size-2xl);
+    margin-bottom: var(--spacing-2);
+    opacity: 0.5;
+  }
+
+  p {
+    margin: 0;
+    font-size: var(--font-size-sm);
+    opacity: 0.8;
+  }
+}
+
+// 滑入动画
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* 响应式断点 */
+@media (max-width: 1440px) {
+  .diagram-box {
+    height: 300px;
+  }
+  
+  .hero-right-inner {
+    height: 300px;
+  }
+}
+
+@media (max-width: 1280px) {
+  .hero-grid {
+    grid-template-columns: 1fr;
+  }
+  
+  .cards-grid {
+    grid-template-columns: 1fr;
+  }
+  
+  .diagram-box {
+    height: 260px;
+  }
+  
+  .hero-right-inner {
+    height: auto; /* 单列布局时不需要固定高度 */
+    justify-content: flex-start;
+    padding-top: 16px;
+  }
 }
 
-::v-deep .el-table .el-table__header-wrapper th,
-.el-table .el-table__fixed-header-wrapper th {
-	background-color: #f6f6f6 !important;
+@media (max-width: 768px) {
+  .calibration-config-simple {
+    .page-header {
+      padding: var(--spacing-4);
+      margin-bottom: var(--spacing-4);
+
+      .header-content {
+        .header-main {
+          .page-title {
+            font-size: var(--font-size-2xl);
+          }
+
+          .page-desc {
+            font-size: var(--font-size-sm);
+          }
+        }
+      }
+
+      .search-section {
+        .search-input {
+          max-width: 100%;
+        }
+      }
+    }
+
+    .page-content {
+      padding: 0 var(--spacing-4);
+    }
+  }
+  
+  .xt-calib {
+    gap: 12px;
+  }
+  
+  .cards-grid {
+    gap: 12px;
+  }
+  
+  .diagram-box {
+    height: 240px;
+  }
+  
+  .hero-right-inner {
+	height: auto;
+    justify-content: flex-start;
+    padding-top: 12px;
+  }
+  
+  .hero-title {
+    font-size: 14px;
+    margin-bottom: 10px;
+  }
+  
+  .hero-list {
+    font-size: 12px;
+    line-height: 1.4;
+    
+    li {
+      margin-bottom: 5px;
+    }
+  }
+  
+  .xt-card {
+    ::v-deep .el-card__header {
+      padding: 16px;
+    }
+    
+    ::v-deep .el-card__body {
+      padding: 12px 16px 16px 16px;
+    }
+  }
+}
+
+// 暗色主题适配
+html.dark {
+  .calibration-config-simple {
+    .page-header {
+      background: var(--color-bg-tertiary);
+      box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+
+      .search-section {
+        .search-input {
+          ::v-deep .el-input__inner {
+            background: var(--color-bg-quaternary);
+            border-color: var(--color-border-tertiary);
+            color: var(--color-text-primary);
+          }
+        }
+      }
+    }
+  }
+  
+  .diagram-hero {
+    background: var(--color-bg-tertiary);
+    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+    
+    &:hover {
+      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+    }
+  }
+  
+  
+  .diagram-caption {
+    color: var(--color-text-tertiary);
+  }
+  
+  .hero-list li b {
+    color: var(--color-text-primary);
+  }
+  
+  .xt-card {
+    background: var(--color-bg-tertiary);
+    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+    
+    &:hover {
+      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+    }
+    
+    ::v-deep .el-card__header {
+      border-bottom-color: var(--color-border-tertiary);
+    }
+    
+    .card-header {
+      .card-title {
+        color: var(--color-text-primary);
+      }
+      
+      .card-desc {
+        color: var(--color-text-quaternary);
+      }
+    }
+  }
+  
+  
+  .empty-state {
+    background: rgba(255, 255, 255, 0.03);
+    border-color: var(--color-border-tertiary);
+  }
 }
 </style>

+ 1211 - 116
src/views/config/rundata/index.vue

@@ -1,116 +1,1211 @@
-<template>
-	<div class="app-container">
-		<div class="hearder">
-			<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
-				<!-- <el-form-item label="参数类型" prop="mapName">
-					<el-input v-model="queryParams.paramName" placeholder="请输入地图名称" clearable />
-				</el-form-item> -->
-				<el-form-item label="参数名称" prop="mapName">
-					<el-input v-model="queryParams.paramType" placeholder="请输入参数名称" clearable />
-				</el-form-item>
-				<el-form-item>
-					<el-button type="primary" icon="el-icon-search" size="mini" @click="">搜索</el-button>
-					<el-button icon="el-icon-refresh" size="mini" @click="">重置</el-button>
-				</el-form-item>
-			</el-form>
-		</div>
-		<el-table :data="data" border style="width: 100%">
-			<el-table-column type="index" width="50" label="序号" align="center"></el-table-column>
-			<el-table-column prop="paramName" label="参数" align="center"></el-table-column>
-			<el-table-column prop="value" label="当前值" align="center">
-				<template slot-scope="scope">
-					<div class="edit-input">
-						<el-input v-model="scope.row.value" placeholder="" :disabled="editingRow !== scope.row"
-							style="width: 60%;" />
-						<el-button type="success" icon="el-icon-check" size="mini" style="margin-left: 10px; padding: 10px 6px;"
-							v-if="editingRow === scope.row" @click="confirmEdit(scope.row)"></el-button>
-						<el-button type="danger" icon="el-icon-close" size="mini" style="padding: 10px 6px;"
-							v-if="editingRow === scope.row" @click="cancelEdit"></el-button>
-					</div>
-				</template>
-			</el-table-column>
-			<el-table-column prop="paramType" label="类型" align="center" :filters="filters"
-				:filter-method="filterType"></el-table-column>
-			<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="80">
-				<template slot-scope="scope">
-					<el-button size="mini" type="text" icon="el-icon-edit-outline" @click="editParams(scope.row)">编辑</el-button>
-				</template>
-			</el-table-column>
-		</el-table>
-	</div>
-</template>
-
-<script>
-export default {
-	data() {
-		return {
-			queryParams: {
-				pageNum: 1,
-				pageSize: 10,
-				paramName: '',
-				paramType: '',
-			},
-			editingRow: null, // 当前正在编辑的行
-			filters: [{ text: '控制参数', value: '控制参数' },
-			{ text: '定位参数', value: '定位参数' },
-			{ text: '避障参数', value: '避障参数' },
-			{ text: '规划参数', value: '规划参数' },
-			{ text: '任务管理', value: '任务管理' }],
-			data: [
-				{ id: '', paramName: '前行障碍物探测距离(m)', paramType: '控制参数', value: 3.0 },
-				{ id: '', paramName: '倒车障碍物探测距离(m)', paramType: '控制参数', value: 1.5 },
-				{ id: '', paramName: '避停距离(m)', paramType: '控制参数', value: '' },
-				{ id: '', paramName: '地图分辨率', paramType: '定位参数', value: 2.0 },
-				{ id: '', paramName: '充电房地图分辨率', paramType: '定位参数', value: 0.3 },
-				{ id: '', paramName: '递推滤波参数', paramType: '定位参数', value: 0.2 },
-				{ id: '', paramName: '激光前方障碍探测起始距离(m)', paramType: '避障参数', value: 0.5 },
-				{ id: '', paramName: '激光后方障碍探测起始距离(m)', paramType: '避障参数', value: -1.0 },
-				{ id: '', paramName: '激光左侧障碍探测起始距离(m)', paramType: '避障参数', value: 0.3 },
-				{ id: '', paramName: '多点循环使能', paramType: '规划参数', value: false },
-				{ id: '', paramName: '多点排序使能', paramType: '规划参数', value: false },
-				{ id: '', paramName: '遇障重规划时间(s)', paramType: '规划参数', value: 30.0 },
-				{ id: '', paramName: '代理服务地址', paramType: '任务管理', value: '' },
-				{ id: '', paramName: '8086映射参数', paramType: '任务管理', value: '' }
-			]
-		}
-	},
-	methods: {
-		// 进入编辑模式
-		editParams(row) {
-			this.editingRow = row; // 设置为当前行
-		},
-		// 确认编辑
-		confirmEdit(row) {
-			// 确认后可以做一些保存操作(如果需要)
-			// todo: 提交修改
-			this.editingRow = null; // 编辑完后恢复禁用
-		},
-		// 取消编辑
-		cancelEdit() {
-			this.editingRow = null; // 取消编辑恢复禁用
-		},
-		filterType(value, row) {
-			return row.paramType === value;
-		},
-	}
-}
-</script>
-
-<style scoped>
-::v-deep .edit-input .el-input.is-disabled .el-input__inner {
-	background-color: #ffffff;
-	border-color: #E4E7ED;
-	color: #616161;
-	cursor: not-allowed;
-	border: none;
-}
-
-::v-deep .edit-input .el-input__inner {
-	text-align: center;
-}
-
-::v-deep .el-table .el-table__header-wrapper th,
-.el-table .el-table__fixed-header-wrapper th {
-  background-color: #f6f6f6 !important;
-}
-</style>
+<template>
+  <div class="runtime-config-simple">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="header-main">
+          <h1 class="page-title">运行参数</h1>
+          <p class="page-desc">配置系统运行参数,逐项编辑保存</p>
+        </div>
+      </div>
+      
+      <!-- 搜索框 -->
+      <div class="search-section">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索运行参数..."
+          clearable
+          prefix-icon="el-icon-search"
+          class="search-input"
+        />
+      </div>
+    </div>
+
+    <!-- 主内容区域 -->
+    <div class="page-content">
+      <div class="config-cards">
+        <!-- 控制参数卡片 - 全宽显示 -->
+        <div class="config-card control-card" v-show="hasVisibleItems('control')">
+          <div class="card-header">
+            <h2 class="card-title">控制参数</h2>
+            <p class="card-desc">车辆运动控制和行驶行为配置</p>
+          </div>
+          <div class="card-content control-content">
+            <XtParamItem
+              v-for="item in controlConfigItems"
+              :key="item.key"
+              v-show="isItemVisible(item)"
+              :label="item.label"
+              :value="config[item.key]"
+              :type="item.type"
+              :unit="item.unit"
+              :placeholder="item.placeholder"
+              :precision="item.precision"
+              :step="item.step"
+              :min="item.min"
+              :max="item.max"
+              :search-keyword="searchKeyword"
+              @save="(value) => handleParamSave(item.key, value)"
+              @cancel="handleParamCancel"
+            />
+            <div v-if="!hasVisibleItems('control')" class="empty-state">
+              <i class="el-icon-search"></i>
+              <p>无匹配的配置参数</p>
+            </div>
+          </div>
+        </div>
+
+        <!-- 其他参数分组 - 两列显示 -->
+        <div class="other-params-grid" :class="{ 'two-columns': isLargeScreen }">
+          <!-- 定位参数卡片 -->
+          <div class="config-card" v-show="hasVisibleItems('positioning')">
+            <div class="card-header">
+              <h2 class="card-title">定位参数</h2>
+              <p class="card-desc">地图和定位算法相关配置</p>
+            </div>
+            <div class="card-content">
+              <XtParamItem
+                v-for="item in positioningConfigItems"
+                :key="item.key"
+                v-show="isItemVisible(item)"
+                :label="item.label"
+                :value="config[item.key]"
+                :type="item.type"
+                :unit="item.unit"
+                :placeholder="item.placeholder"
+                :precision="item.precision"
+                :step="item.step"
+                :min="item.min"
+                :max="item.max"
+                :search-keyword="searchKeyword"
+                @save="(value) => handleParamSave(item.key, value)"
+                @cancel="handleParamCancel"
+              />
+              <div v-if="!hasVisibleItems('positioning')" class="empty-state">
+                <i class="el-icon-search"></i>
+                <p>无匹配的配置参数</p>
+              </div>
+            </div>
+          </div>
+
+          <!-- 避障参数卡片 -->
+          <div class="config-card" v-show="hasVisibleItems('obstacle')">
+            <div class="card-header">
+              <h2 class="card-title">避障参数</h2>
+              <p class="card-desc">激光雷达障碍物检测配置</p>
+            </div>
+            <div class="card-content">
+              <XtParamItem
+                v-for="item in obstacleConfigItems"
+                :key="item.key"
+                v-show="isItemVisible(item)"
+                :label="item.label"
+                :value="config[item.key]"
+                :type="item.type"
+                :unit="item.unit"
+                :placeholder="item.placeholder"
+                :precision="item.precision"
+                :step="item.step"
+                :min="item.min"
+                :max="item.max"
+                :search-keyword="searchKeyword"
+                @save="(value) => handleParamSave(item.key, value)"
+                @cancel="handleParamCancel"
+              />
+              <div v-if="!hasVisibleItems('obstacle')" class="empty-state">
+                <i class="el-icon-search"></i>
+                <p>无匹配的配置参数</p>
+              </div>
+            </div>
+          </div>
+
+          <!-- 规划参数卡片 -->
+          <div class="config-card" v-show="hasVisibleItems('planning')">
+            <div class="card-header">
+              <h2 class="card-title">规划参数</h2>
+              <p class="card-desc">路径规划和任务执行配置</p>
+            </div>
+            <div class="card-content">
+              <XtParamItem
+                v-for="item in planningConfigItems"
+                :key="item.key"
+                v-show="isItemVisible(item)"
+                :label="item.label"
+                :value="config[item.key]"
+                :type="item.type"
+                :unit="item.unit"
+                :placeholder="item.placeholder"
+                :precision="item.precision"
+                :step="item.step"
+                :min="item.min"
+                :max="item.max"
+                :options="item.options"
+                :search-keyword="searchKeyword"
+                @save="(value) => handleParamSave(item.key, value)"
+                @cancel="handleParamCancel"
+              />
+              <div v-if="!hasVisibleItems('planning')" class="empty-state">
+                <i class="el-icon-search"></i>
+                <p>无匹配的配置参数</p>
+              </div>
+            </div>
+          </div>
+
+          <!-- 任务管理卡片 -->
+          <div class="config-card" v-show="hasVisibleItems('task')">
+            <div class="card-header">
+              <h2 class="card-title">任务管理</h2>
+              <p class="card-desc">自主充电和任务管理配置</p>
+            </div>
+            <div class="card-content">
+              <XtParamItem
+                v-for="item in taskConfigItems"
+                :key="item.key"
+                v-show="isItemVisible(item)"
+                :label="item.label"
+                :value="config[item.key]"
+                :type="item.type"
+                :unit="item.unit"
+                :placeholder="item.placeholder"
+                :precision="item.precision"
+                :step="item.step"
+                :min="item.min"
+                :max="item.max"
+                :options="item.options"
+                :search-keyword="searchKeyword"
+                @save="(value) => handleParamSave(item.key, value)"
+                @cancel="handleParamCancel"
+              />
+              <div v-if="!hasVisibleItems('task')" class="empty-state">
+                <i class="el-icon-search"></i>
+                <p>无匹配的配置参数</p>
+              </div>
+            </div>
+          </div>
+        </div>
+		</div>
+					</div>
+	</div>
+</template>
+
+<script>
+import XtParamItem from '@/components/XtParamItem'
+
+// 尝试导入真实 API,失败则使用 Mock
+let runtimeApi
+try {
+  runtimeApi = require('@/api/config/runtime')
+} catch (error) {
+  // Fallback 到 Mock API
+  runtimeApi = {
+    getRuntimeParams: () => Promise.resolve({ code: 200, data: {} }),
+    updateParam: ({ key, value }) => Promise.resolve({ code: 200, message: '参数保存成功' })
+  }
+}
+
+export default {
+  name: 'RuntimeConfig',
+  
+  components: {
+    XtParamItem
+  },
+  
+	data() {
+		return {
+      // 加载状态
+      loading: false,
+      
+      // 搜索关键词
+      searchKeyword: '',
+      
+      // 屏幕尺寸
+      isLargeScreen: false,
+      
+      // 配置数据
+      config: {
+        // 控制参数
+        forwardObstacleDetectionDistance: 3.0,
+        reverseObstacleDetectionDistance: 1.5,
+        avoidStopDistance: 0.5,
+        forwardMaxFollowDistance: 5.0,
+        forwardMinFollowDistance: 1.0,
+        forwardFollowSpeedRatio: 0.8,
+        reverseMinFollowDistance: 1.0,
+        reverseFollowSpeedRatio: 0.6,
+        decelerationCTEStartValue: 0.5,
+        decelerationCTEMaxValue: 1.0,
+        maxCTE: 2.0,
+        maxDrivingSpeed: 2.0,
+        maxReverseSpeed: 1.0,
+        maxAcceleration: 1.0,
+        maxDeceleration: 2.0,
+        startupPhaseEndSpeed: 0.2,
+        terminationPhaseEndSpeed: 0.1,
+        maxAngularVelocity: 1.0,
+        maxSteeringAngle: 0.5,
+        lateralAcceleration: 1.5,
+        deceleration: 2.0,
+        inPlaceRotationAngleErrorTolerance: 0.1,
+        nonInPlaceRotationStartupMaxAngle: 0.3,
+        inPlaceRotationAngularVelocityRatio: 0.5,
+        drivingSteeringAngularVelocityRatio: 0.8,
+        reverseFeedCycles: 3,
+        forwardPositioningForwardOffset: 0.5,
+        reversePositioningRearOffset: 0.5,
+        targetPointForwardDistance: 1.0,
+        routeDistance: 10.0,
+        obstacleDistance: 2.0,
+        maxBypassAngle: 45,
+        minBypassAngle: 15,
+        bypassKeepDistance: 1.0,
+        controller: 'PID',
+        odometerTopicName: '/odom',
+        enableDebugMode: false,
+        maxPassageWidth: 3.0,
+        minPassageWidth: 1.5,
+        
+        // 定位参数
+        mapResolution: 0.05,
+        chargingRoomMapResolution: 0.02,
+        recursiveFilterParam: 0.8,
+        filterParam: 0.9,
+        chargingRoomFilterParam: 0.7,
+        chargingRoomPositioningStep: 0.1,
+        mapUpdateInterpolationDistance: 0.5,
+        positioningCorrectionPointCloudTopicName: '/cloud_corrected',
+        deadReckoningPointCloudTopicName: '/cloud_dead_reckoning',
+        
+        // 避障参数
+        laserForwardObstacleDetectionStartDistance: 0.5,
+        laserRearObstacleDetectionStartDistance: -1.0,
+        laserLeftObstacleDetectionStartDistance: 0.3,
+        laserRightObstacleDetectionStartDistance: 0.3,
+        
+        // 规划参数
+        multiPointCycleEnabled: false,
+        multiPointSortEnabled: false,
+        obstacleReplanningTime: 30,
+        
+        // 任务管理
+        autonomousChargingEnabled: false,
+        autonomousChargingLowBatteryThreshold: 0.2
+      },
+      
+      // 控制参数配置项
+      controlConfigItems: [
+        {
+          key: 'forwardObstacleDetectionDistance',
+          label: '前行障碍物探测距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'reverseObstacleDetectionDistance',
+          label: '倒车障碍物探测距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'avoidStopDistance',
+          label: '避停距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'forwardMaxFollowDistance',
+          label: '前行最大跟点距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'forwardMinFollowDistance',
+          label: '前行最小跟点距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'forwardFollowSpeedRatio',
+          label: '前行跟点/速度系数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'reverseMinFollowDistance',
+          label: '倒车最小跟点距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'reverseFollowSpeedRatio',
+          label: '倒车跟点/速度系数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'decelerationCTEStartValue',
+          label: '减速/CTE起始值',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'decelerationCTEMaxValue',
+          label: '减速/CTE最大值',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'maxCTE',
+          label: '最大CTE',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'maxDrivingSpeed',
+          label: '最大行驶速度',
+          type: 'number',
+          unit: 'm/s',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'maxReverseSpeed',
+          label: '最大倒车速度',
+          type: 'number',
+          unit: 'm/s',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'maxAcceleration',
+          label: '最大加速度',
+          type: 'number',
+          unit: 'm/s²',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'maxDeceleration',
+          label: '最大减速度',
+          type: 'number',
+          unit: 'm/s²',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'startupPhaseEndSpeed',
+          label: '启动阶段截止速度',
+          type: 'number',
+          unit: 'm/s',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'terminationPhaseEndSpeed',
+          label: '终止阶段截止速度',
+          type: 'number',
+          unit: 'm/s',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'maxAngularVelocity',
+          label: '最大角速度',
+          type: 'number',
+          unit: 'rad/s',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'maxSteeringAngle',
+          label: '最大转向角度',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0',
+          precision: 3,
+          step: 0.001,
+          min: 0
+        },
+        {
+          key: 'lateralAcceleration',
+          label: '侧向加速度',
+          type: 'number',
+          unit: 'm/s²',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'deceleration',
+          label: '减速度',
+          type: 'number',
+          unit: 'm/s²',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'inPlaceRotationAngleErrorTolerance',
+          label: '原地旋转角度误差容限',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0',
+          precision: 3,
+          step: 0.001,
+          min: 0
+        },
+        {
+          key: 'nonInPlaceRotationStartupMaxAngle',
+          label: '非原地旋转起步最大角度',
+          type: 'number',
+          unit: 'rad',
+          placeholder: '0.0',
+          precision: 3,
+          step: 0.001,
+          min: 0
+        },
+        {
+          key: 'inPlaceRotationAngularVelocityRatio',
+          label: '原地旋转角速度系数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'drivingSteeringAngularVelocityRatio',
+          label: '行驶转向角速度系数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'reverseFeedCycles',
+          label: '倒车进给周期数',
+          type: 'number',
+          placeholder: '0',
+          step: 1,
+          min: 0
+        },
+        {
+          key: 'forwardPositioningForwardOffset',
+          label: '前行定位相对车中心前向偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01
+        },
+        {
+          key: 'reversePositioningRearOffset',
+          label: '倒车定位相对车中心后向偏移',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01
+        },
+        {
+          key: 'targetPointForwardDistance',
+          label: '目标点前移距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'routeDistance',
+          label: '路线距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'obstacleDistance',
+          label: '障碍距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'maxBypassAngle',
+          label: '最大绕障角度',
+          type: 'number',
+          unit: '°',
+          placeholder: '0',
+          step: 1,
+          min: 0,
+          max: 180
+        },
+        {
+          key: 'minBypassAngle',
+          label: '最小绕障角度',
+          type: 'number',
+          unit: '°',
+          placeholder: '0',
+          step: 1,
+          min: 0,
+          max: 180
+        },
+        {
+          key: 'bypassKeepDistance',
+          label: '绕障后保持距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'controller',
+          label: '控制器',
+          type: 'text',
+          placeholder: '请输入控制器类型'
+        },
+        {
+          key: 'odometerTopicName',
+          label: '里程计主题名',
+          type: 'text',
+          placeholder: '请输入主题名'
+        },
+        {
+          key: 'enableDebugMode',
+          label: '开启调试模式',
+          type: 'switch'
+        },
+        {
+          key: 'maxPassageWidth',
+          label: '最大通行宽度',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        },
+        {
+          key: 'minPassageWidth',
+          label: '最小通行宽度',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1,
+          min: 0
+        }
+      ],
+      
+      // 定位参数配置项
+      positioningConfigItems: [
+        {
+          key: 'mapResolution',
+          label: '地图分辨率',
+          type: 'number',
+          placeholder: '0.00',
+          precision: 3,
+          step: 0.001,
+          min: 0
+        },
+        {
+          key: 'chargingRoomMapResolution',
+          label: '充电房地图分辨率',
+          type: 'number',
+          placeholder: '0.00',
+          precision: 3,
+          step: 0.001,
+          min: 0
+        },
+        {
+          key: 'recursiveFilterParam',
+          label: '递推滤波参数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'filterParam',
+          label: '滤波参数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'chargingRoomFilterParam',
+          label: '充电房滤波参数',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        },
+        {
+          key: 'chargingRoomPositioningStep',
+          label: '充电房定位步长',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'mapUpdateInterpolationDistance',
+          label: '地图更新插帧距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0
+        },
+        {
+          key: 'positioningCorrectionPointCloudTopicName',
+          label: '定位修正点云主题名',
+          type: 'text',
+          placeholder: '请输入主题名'
+        },
+        {
+          key: 'deadReckoningPointCloudTopicName',
+          label: '推算点云主题名',
+          type: 'text',
+          placeholder: '请输入主题名'
+        }
+      ],
+      
+      // 避障参数配置项
+      obstacleConfigItems: [
+        {
+          key: 'laserForwardObstacleDetectionStartDistance',
+          label: '激光前方障碍探测起始距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1
+        },
+        {
+          key: 'laserRearObstacleDetectionStartDistance',
+          label: '激光后方障碍探测起始距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1
+        },
+        {
+          key: 'laserLeftObstacleDetectionStartDistance',
+          label: '激光左侧障碍探测起始距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1
+        },
+        {
+          key: 'laserRightObstacleDetectionStartDistance',
+          label: '激光右侧障碍探测起始距离',
+          type: 'number',
+          unit: 'm',
+          placeholder: '0.0',
+          precision: 1,
+          step: 0.1
+        }
+      ],
+      
+      // 规划参数配置项
+      planningConfigItems: [
+        {
+          key: 'multiPointCycleEnabled',
+          label: '多点循环使能',
+          type: 'switch'
+        },
+        {
+          key: 'multiPointSortEnabled',
+          label: '多点排序使能',
+          type: 'switch'
+        },
+        {
+          key: 'obstacleReplanningTime',
+          label: '遇障重规划时间',
+          type: 'number',
+          unit: 's',
+          placeholder: '0',
+          step: 1,
+          min: 0
+        }
+      ],
+      
+      // 任务管理配置项
+      taskConfigItems: [
+        {
+          key: 'autonomousChargingEnabled',
+          label: '自主充电使能',
+          type: 'switch'
+        },
+        {
+          key: 'autonomousChargingLowBatteryThreshold',
+          label: '自主充电低电量阈值',
+          type: 'number',
+          placeholder: '0.0',
+          precision: 2,
+          step: 0.01,
+          min: 0,
+          max: 1
+        }
+      ]
+    }
+  },
+  
+  async created() {
+    await this.loadConfig()
+    this.calculateScreenSize()
+    window.addEventListener('resize', this.calculateScreenSize)
+  },
+  
+  beforeDestroy() {
+    window.removeEventListener('resize', this.calculateScreenSize)
+  },
+  
+	methods: {
+    // 加载配置
+    async loadConfig() {
+      this.loading = true
+      try {
+        const response = await runtimeApi.getRuntimeParams()
+        if (response.code === 200 && response.data) {
+          this.config = { ...this.config, ...response.data }
+        }
+      } catch (error) {
+        console.warn('加载运行参数失败,使用默认值:', error)
+        this.$message.warning('加载配置失败,使用默认参数')
+      } finally {
+        this.loading = false
+      }
+    },
+
+    // 保存单个参数
+    async handleParamSave(key, value) {
+      try {
+        const response = await runtimeApi.updateParam({ key, value })
+        if (response.code === 200) {
+          this.config[key] = value
+          // 不显示保存成功消息,由组件内部显示
+        } else {
+          throw new Error(response.message || '保存失败')
+        }
+      } catch (error) {
+        this.$message.error('保存失败: ' + error.message)
+        throw error // 重新抛出错误,让组件显示错误状态
+      }
+    },
+
+    // 取消参数编辑
+    handleParamCancel() {
+      // 可以在这里添加取消编辑的逻辑
+      console.log('参数编辑已取消')
+    },
+
+    // 检查项目是否可见(搜索过滤)
+    isItemVisible(item) {
+      if (!this.searchKeyword.trim()) {
+        return true
+      }
+      const keyword = this.searchKeyword.toLowerCase().trim()
+      return item.label.toLowerCase().includes(keyword)
+    },
+
+    // 检查分组是否有可见项目
+    hasVisibleItems(groupType) {
+      let items = []
+      switch (groupType) {
+        case 'control':
+          items = this.controlConfigItems
+          break
+        case 'positioning':
+          items = this.positioningConfigItems
+          break
+        case 'obstacle':
+          items = this.obstacleConfigItems
+          break
+        case 'planning':
+          items = this.planningConfigItems
+          break
+        case 'task':
+          items = this.taskConfigItems
+          break
+        default:
+          return false
+      }
+      
+      return items.some(item => this.isItemVisible(item))
+    },
+
+    // 计算屏幕尺寸
+    calculateScreenSize() {
+      this.isLargeScreen = window.innerWidth >= 1440
+    }
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.runtime-config-simple {
+  min-height: 100vh;
+  background: var(--color-bg-secondary);
+  padding-bottom: var(--spacing-6);
+
+  .page-header {
+    background: var(--color-bg-card);
+    border-bottom: 1px solid var(--color-border-primary);
+    padding: var(--spacing-6);
+    margin-bottom: var(--spacing-6);
+    box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+
+    .header-content {
+      max-width: 1200px;
+      margin: 0 auto;
+
+      .header-main {
+        .page-title {
+          color: var(--color-text-primary);
+          font-size: var(--font-size-3xl);
+          font-weight: var(--font-weight-bold);
+          margin: 0 0 var(--spacing-2) 0;
+          line-height: var(--line-height-tight);
+        }
+
+        .page-desc {
+          color: var(--color-text-secondary);
+          font-size: var(--font-size-base);
+          margin: 0 0 var(--spacing-4) 0;
+          line-height: var(--line-height-relaxed);
+        }
+      }
+    }
+
+    .search-section {
+      max-width: 1200px;
+      margin: 0 auto;
+
+      .search-input {
+        max-width: 400px;
+
+        ::v-deep .el-input__inner {
+          border-radius: var(--radius-lg);
+          border: 1px solid var(--color-border-primary);
+          background: var(--color-bg-card);
+          transition: all var(--duration-200) var(--ease-out);
+
+          &:focus {
+            border-color: var(--color-primary);
+            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+          }
+        }
+
+        ::v-deep .el-input__prefix {
+          color: var(--color-text-tertiary);
+        }
+      }
+    }
+  }
+
+  .page-content {
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 var(--spacing-6);
+  }
+}
+
+/* 配置卡片网格 */
+.config-cards {
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-5);
+}
+
+/* 控制参数卡片 - 全宽显示 */
+.control-card {
+  width: 100%;
+  
+  .control-content {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+    gap: 0;
+    
+    @media (max-width: 1024px) {
+      grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
+    }
+    
+    @media (max-width: 768px) {
+      grid-template-columns: 1fr;
+    }
+  }
+}
+
+/* 其他参数分组网格 */
+.other-params-grid {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: var(--spacing-5);
+  
+  &.two-columns {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+/* 配置卡片样式 */
+.config-card {
+  background: var(--color-bg-card);
+  border-radius: var(--radius-xl);
+  box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+  border: 1px solid var(--color-border-primary);
+  transition: all var(--duration-200) var(--ease-out);
+  animation: slideInUp 0.3s ease-out;
+  
+  &:hover {
+    box-shadow: 0 8px 24px rgba(2, 6, 23, 0.08);
+    border-color: var(--color-border-secondary);
+  }
+  
+  .card-header {
+    padding: var(--spacing-5);
+    border-bottom: 1px solid var(--color-border-secondary);
+    
+    .card-title {
+      font-size: var(--font-size-lg);
+      font-weight: var(--font-weight-semibold);
+      color: var(--color-text-primary);
+      margin: 0 0 var(--spacing-1) 0;
+      line-height: var(--line-height-tight);
+    }
+    
+    .card-desc {
+      font-size: var(--font-size-sm);
+      color: var(--color-text-tertiary);
+      margin: 0;
+      line-height: var(--line-height-relaxed);
+      opacity: 0.8;
+    }
+  }
+  
+  .card-content {
+    padding: var(--spacing-4) var(--spacing-5) var(--spacing-5);
+  }
+}
+
+/* 空状态样式 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 120px;
+  color: var(--color-text-quaternary);
+  background: rgba(0, 0, 0, 0.02);
+  border-radius: var(--radius-base);
+  border: 1px dashed var(--color-border-secondary);
+
+  i {
+    font-size: var(--font-size-2xl);
+    margin-bottom: var(--spacing-2);
+    opacity: 0.5;
+  }
+
+  p {
+    margin: 0;
+    font-size: var(--font-size-sm);
+    opacity: 0.8;
+  }
+}
+
+// 滑入动画
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* 响应式断点 */
+@media (max-width: 768px) {
+  .runtime-config-simple {
+    .page-header {
+      padding: var(--spacing-4);
+      margin-bottom: var(--spacing-4);
+
+      .header-content {
+        .header-main {
+          .page-title {
+            font-size: var(--font-size-2xl);
+          }
+
+          .page-desc {
+            font-size: var(--font-size-sm);
+          }
+        }
+      }
+
+      .search-section {
+        .search-input {
+          max-width: 100%;
+        }
+      }
+    }
+
+    .page-content {
+      padding: 0 var(--spacing-4);
+    }
+  }
+  
+  .config-cards {
+    gap: var(--spacing-4);
+  }
+  
+  .other-params-grid {
+    grid-template-columns: 1fr !important;
+    gap: var(--spacing-4);
+  }
+  
+  .config-card {
+    .card-header {
+      padding: var(--spacing-4);
+    }
+    
+    .card-content {
+      padding: var(--spacing-3) var(--spacing-4) var(--spacing-4);
+    }
+  }
+}
+
+// 暗色主题适配
+html.dark {
+  .runtime-config-simple {
+    .page-header {
+      background: var(--color-bg-tertiary);
+      box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+
+      .search-section {
+        .search-input {
+          ::v-deep .el-input__inner {
+            background: var(--color-bg-quaternary);
+            border-color: var(--color-border-tertiary);
+            color: var(--color-text-primary);
+          }
+        }
+      }
+    }
+  }
+  
+  .config-card {
+    background: var(--color-bg-tertiary);
+    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+    border-color: var(--color-border-tertiary);
+    
+    &:hover {
+      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+      border-color: var(--color-border-secondary);
+    }
+    
+    .card-header {
+      border-bottom-color: var(--color-border-tertiary);
+      
+      .card-title {
+        color: var(--color-text-primary);
+      }
+      
+      .card-desc {
+        color: var(--color-text-quaternary);
+      }
+    }
+  }
+  
+  .empty-state {
+    background: rgba(255, 255, 255, 0.03);
+    border-color: var(--color-border-tertiary);
+  }
+}
+</style>

+ 1514 - 153
src/views/devops/version/index.vue

@@ -1,153 +1,1514 @@
-<template>
-  <div class="app-container">
-    <div class="version-card">
-      <el-card class="box-card" shadow="always">
-        <div slot="header" class="clearfix">
-          <span>服务器版本</span>
-          <el-button style="float: right; padding: 3px 5px" type="success">版本导出</el-button>
-        </div>
-        <div class="text-item" v-for="item in versionData">
-          <el-row style="height: 100%; display: flex;align-items: center;  ">
-            <el-col :span="6">
-              <div class="grid-content">{{ item.name }}</div>
-            </el-col>
-            <el-col :span="16">
-              <div class="grid-content">{{ item.value }}</div>
-            </el-col>
-            <el-col :span="2">
-              <div class="grid-content"><el-button style="float: right; padding: 3px 0" type="text">卸载</el-button></div>
-            </el-col>
-          </el-row>
-        </div>
-      </el-card>
-    </div>
-    <div style="margin-top: 30px;position: relative;">
-      <h4>更新版本</h4>
-      <div style="position: absolute;right: 0;top: 0;">
-        <el-row>
-          <el-button type="info" size="mini" @click="install">全部安装</el-button>
-        </el-row>
-      </div>
-      <el-table :data="softData" border style="width: 100%">
-        <el-table-column type="index" width="50" label="序号" align="center"></el-table-column>
-        <el-table-column prop="name" label="文件名" width="280" align="center">
-        </el-table-column>
-        <el-table-column prop="address" label="编辑">
-        </el-table-column>
-      </el-table>
-    </div>
-    <div style="margin-top: 20px;">
-      <IndexHand :fileType="['deb', 'tar', 'zip']" :fileSize="100" @input="fileListBack"></IndexHand>
-    </div>
-  </div>
-</template>
-
-<script>
-import IndexHand from "@/components/FileUpload/index-hand";
-
-export default {
-  components: { IndexHand },
-  data() {
-    return {
-      versionData: [
-        {
-          name: 'leador-data-analyser',
-          value: '192.168.10.10'
-        },
-        {
-          name: '激光网卡',
-          value: '0.3.1'
-        },
-        {
-          name: 'leador-robot-config-web',
-          value: '0.7.133'
-        },
-        {
-          name: 'ros-kinetic-leador-auto-pursuit-local-planner',
-          value: '0.0.1'
-        },
-        {
-          name: 'ros-kinetic-leador-en-control-msgs',
-          value: '1.0.0'
-        },
-        {
-          name: 'ros-kinetic-leador-ld-sensor-msgs',
-          value: '2.0.1'
-        }
-      ],
-      softData: []
-    }
-  },
-  methods: {
-    fileListBack(data) {
-      const fileName = data.split('/').pop(); // 获取文件名
-      let file = { name: fileName };
-      // 检查 softData 中是否已经存在该文件名
-      const index = this.softData.findIndex(item => item.name === fileName);
-      if (index === -1) {
-        // 如果不存在,则追加
-        this.softData.push(file);
-      } else {
-        // 如果存在,则删除该文件
-        this.softData.splice(index, 1);
-      }
-    },
-    // 全部安装
-    install() {
-      if (this.softData.length < 1) {
-        this.$message({
-          message: '请先上传对应文件!',
-          type: 'warning'
-        });
-        return;
-      }
-      this.$confirm('安装已上传的所有文件包?', '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        this.$message({
-          type: 'success',
-          message: '已开始安装!'
-        });
-      }).catch(() => {});
-    }
-  }
-}
-</script>
-
-<style scoped>
-::v-deep .version-card .el-card {
-  border-radius: 8px;
-}
-
-::v-deep .version-card .el-card.is-always-shadow,
-.el-card.is-hover-shadow:focus,
-.el-card.is-hover-shadow:hover {
-  box-shadow: 0 2px 12px 0 rgba(161, 161, 161, 0.1);
-}
-
-::v-deep .version-card .el-card__header {
-  padding: 12px 20px
-}
-
-::v-deep .version-card .el-card__body,
-.el-main {
-  padding: 0;
-}
-
-.text-item {
-  width: 100%;
-  height: 50px;
-  border-bottom: 1px solid #EBEEF5;
-}
-
-.grid-content {
-  padding: 0 20px;
-}
-
-::v-deep .el-table .el-table__header-wrapper th,
-.el-table .el-table__fixed-header-wrapper th {
-	background-color: #f6f6f6 !important;
-}
-</style>
+<template>
+  <div class="version-management">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="header-main">
+          <h1 class="page-title">版本管理</h1>
+          <p class="page-desc">管理车载系统软件版本,支持查看已安装版本和上传安装新版本</p>
+        </div>
+      </div>
+      
+      <!-- 搜索框 -->
+      <div class="search-section">
+        <el-input
+          v-model="searchQuery"
+          placeholder="搜索已安装版本..."
+          clearable
+          prefix-icon="el-icon-search"
+          class="search-input"
+          @input="handleSearch"
+        />
+      </div>
+    </div>
+
+    <!-- 主内容区域 -->
+    <div class="page-content">
+      <!-- 车端已安装版本卡片 -->
+    <div class="version-card">
+        <div class="card-header">
+          <h2 class="card-title">车端已安装版本</h2>
+          <p class="card-desc">当前车载系统已安装的软件组件与版本</p>
+        </div>
+        <div class="card-content">
+          <div class="table-wrapper">
+            <el-table 
+              :data="installedVersions" 
+              v-loading="loadingInstalled"
+              border 
+              stripe
+              style="width: 100%"
+            >
+            <el-table-column type="index" width="100" label="序号" align="center" />
+            <el-table-column prop="name" label="组件/软件" min-width="220" show-overflow-tooltip />
+            <el-table-column prop="version" label="当前版本" width="140" align="center" />
+            <el-table-column prop="updateTime" label="更新时间" width="200" align="center" show-overflow-tooltip />
+            <el-table-column label="操作" width="100" align="center">
+              <template slot-scope="{ row }">
+                <el-button 
+                  type="text" 
+                  size="mini" 
+                  @click="handleUninstall(row)"
+                  :loading="row.uninstalling"
+                >
+                  卸载
+                </el-button>
+              </template>
+            </el-table-column>
+            </el-table>
+          </div>
+          
+          <!-- 分页 -->
+          <div class="pagination-wrapper">
+            <el-pagination
+              @size-change="handleSizeChange"
+              @current-change="handleCurrentChange"
+              :current-page="pagination.pageNum"
+              :page-sizes="[10, 20, 50, 100]"
+              :page-size="pagination.pageSize"
+              layout="total, sizes, prev, pager, next, jumper"
+              :total="pagination.total"
+            />
+          </div>
+        </div>
+        </div>
+
+      <!-- 待安装列表卡片 -->
+      <div class="version-card">
+        <div class="card-header">
+          <h2 class="card-title">待安装列表</h2>
+          <p class="card-desc">管理上传的安装包文件,支持 .deb / .tar / .zip 格式</p>
+        </div>
+        <div class="card-content">
+          <!-- 工具栏 -->
+          <div class="install-toolbar">
+            <div class="toolbar-actions">
+              <el-button 
+                type="success" 
+                icon="el-icon-upload2"
+                size="small" 
+                @click="showUploadDialog"
+                :disabled="installing"
+                aria-label="上传安装包"
+              >
+                上传安装包
+              </el-button>
+              <el-button 
+                type="primary" 
+                icon="el-icon-download"
+                size="small" 
+                @click="installAll"
+                :disabled="!installTasks.length || installing"
+                :loading="installing"
+                aria-label="批量安装所有文件"
+              >
+                全部安装
+              </el-button>
+              <el-button 
+                size="small" 
+                type="danger" 
+                icon="el-icon-delete"
+                plain
+                @click="clearList"
+                :disabled="installing"
+                aria-label="清空待安装列表"
+              >
+                清空列表
+              </el-button>
+    </div>
+      </div>
+          
+          <!-- 表格区域 -->
+          <div class="table-container">
+            <div v-if="installTasks.length > 0" class="table-wrapper">
+              <el-table 
+                :data="installTasks" 
+                height="420"
+                border 
+                stripe
+                style="width: 100%"
+              >
+              <el-table-column type="index" width="100" label="序号" align="center" />
+              <el-table-column prop="fileName" label="文件名" min-width="300" show-overflow-tooltip>
+                <template slot-scope="{ row }">
+                  <div class="file-name-cell">{{ row.fileName }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="大小" width="130" align="center">
+                <template slot-scope="{ row }">
+                  {{ formatFileSize(row.size) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="类型" width="90" align="center">
+                <template slot-scope="{ row }">
+                  {{ getFileType(row.fileName) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="状态" width="110" align="center">
+                <template slot-scope="{ row }">
+                  <el-tag 
+                    :type="getStatusType(row.status)" 
+                    size="mini"
+                  >
+                    {{ row.status }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="180" align="center" :show-overflow-tooltip="false">
+                <template slot-scope="{ row, $index }">
+                  <div class="action-buttons">
+                    <el-button 
+                      type="text" 
+                      size="mini" 
+                      @click="handleInstall(row, $index)"
+                      :disabled="installing"
+                      :aria-label="`${row.status === '失败' ? '重试安装' : '安装'} ${row.fileName}`"
+                    >
+                      {{ row.status === '失败' ? '重试' : '安装' }}
+                    </el-button>
+                    <el-button 
+                      type="text" 
+                      size="mini" 
+                      @click="handleDelete(row, $index)"
+                      :disabled="installing"
+                      :aria-label="`删除 ${row.fileName}`"
+                    >
+                      删除
+                    </el-button>
+                  </div>
+                </template>
+        </el-table-column>
+      </el-table>
+    </div>
+            
+            <!-- 空状态 -->
+            <div v-else class="empty-container">
+              <el-empty 
+                description="暂无安装文件" 
+                :image-size="120"
+              >
+                <el-button 
+                  type="primary" 
+                  @click="showUploadDialog"
+                  :disabled="installing"
+                >
+                  上传安装包
+                </el-button>
+              </el-empty>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 上传弹框 -->
+      <el-dialog
+        title="上传安装包"
+        :visible.sync="uploadDialogVisible"
+        width="600px"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        custom-class="upload-dialog"
+      >
+        <div class="upload-dialog-content">
+          <div class="upload-area">
+            <div class="upload-container">
+              <el-upload
+                ref="dialogUpload"
+                :action="uploadAction"
+                :multiple="true"
+                :drag="true"
+                :accept="'.deb,.tar,.zip'"
+                :before-upload="beforeUpload"
+                :on-progress="onUploadProgress"
+                :on-success="onUploadSuccess"
+                :on-error="onUploadError"
+                :show-file-list="false"
+                :disabled="installing"
+                class="dialog-upload-dragger"
+              >
+                <i class="el-icon-upload"></i>
+                <div class="el-upload__text">
+                  将文件拖拽到此处,或<em>点击选择文件</em>
+                </div>
+              </el-upload>
+              <div class="upload-format-hint">
+                支持 .deb、.tar、.zip 格式文件,单个文件不超过 100MB,可同时选择多个文件
+              </div>
+            </div>
+          </div>
+          
+          <!-- 上传进度显示 -->
+          <div v-if="uploadingFiles.length > 0" class="upload-progress">
+            <h4>上传进度</h4>
+            <div 
+              v-for="file in uploadingFiles" 
+              :key="file.uid"
+              class="upload-item"
+            >
+              <div class="upload-item-info">
+                <span class="file-name">{{ file.name }}</span>
+                <span class="file-size">{{ formatFileSize(file.size) }}</span>
+              </div>
+              <el-progress 
+                :percentage="file.percentage" 
+                :status="file.status"
+                :stroke-width="6"
+              />
+            </div>
+          </div>
+        </div>
+        
+        <div slot="footer" class="dialog-footer">
+          <el-button plain @click="closeUploadDialog">取消</el-button>
+          <el-button 
+            type="primary" 
+            @click="closeUploadDialog"
+            :disabled="uploadingFiles.some(f => f.status === 'uploading')"
+          >
+            完成
+          </el-button>
+    </div>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+
+<script>
+import { apiWithFallback } from '@/api/devops/version'
+
+export default {
+  name: 'VersionManagement',
+  
+  data() {
+    return {
+      // API 待接入 - 当前使用Mock API作为兜底
+      uploadAction: '/api/devops/versions/upload',
+      
+      // 搜索相关
+      searchQuery: '',
+      searchTimer: null,
+      
+      // 已安装版本列表
+      installedVersions: [],
+      loadingInstalled: false,
+      pagination: {
+        pageNum: 1,
+        pageSize: 10,
+        total: 0
+      },
+      
+      // 待安装列表
+      installTasks: [],
+      installing: false,
+      
+      // 上传弹框
+      uploadDialogVisible: false,
+      uploadingFiles: [],
+      
+      // 允许的文件类型和大小限制
+      allowedTypes: ['.deb', '.tar', '.zip'],
+      maxFileSize: 100 * 1024 * 1024 // 100MB
+    }
+  },
+  
+  async created() {
+    await this.loadInstalledVersions()
+    await this.loadUploadList()
+  },
+  
+  methods: {
+    // 加载已安装版本列表
+    async loadInstalledVersions() {
+      this.loadingInstalled = true
+      try {
+        const params = {
+          pageNum: this.pagination.pageNum,
+          pageSize: this.pagination.pageSize,
+          search: this.searchQuery
+        }
+        
+        const response = await apiWithFallback.getInstalledVersions(params)
+        if (response.code === 200) {
+          this.installedVersions = response.data.list || []
+          this.pagination.total = response.data.total || 0
+        }
+      } catch (error) {
+        this.$message.error('加载已安装版本失败: ' + error.message)
+      } finally {
+        this.loadingInstalled = false
+      }
+    },
+    
+    // 加载上传文件列表
+    async loadUploadList() {
+      try {
+        const response = await apiWithFallback.getUploadList()
+        if (response.code === 200) {
+          this.installTasks = response.data.list || []
+        }
+      } catch (error) {
+        this.$message.error('加载上传列表失败: ' + error.message)
+      }
+    },
+    
+    // 搜索处理(防抖)
+    handleSearch() {
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+      }
+      this.searchTimer = setTimeout(() => {
+        this.pagination.pageNum = 1
+        this.loadInstalledVersions()
+      }, 300)
+    },
+    
+    // 分页处理
+    handleSizeChange(val) {
+      this.pagination.pageSize = val
+      this.pagination.pageNum = 1
+      this.loadInstalledVersions()
+    },
+    
+    handleCurrentChange(val) {
+      this.pagination.pageNum = val
+      this.loadInstalledVersions()
+    },
+    
+    // 卸载软件包
+    async handleUninstall(row) {
+      try {
+        await this.$confirm('确定要卸载 "' + row.name + '" 吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        
+        this.$set(row, 'uninstalling', true)
+        
+        const response = await apiWithFallback.uninstallPackage({
+          id: row.id,
+          name: row.name
+        })
+        
+        if (response.code === 200) {
+          this.$message.success('卸载成功')
+          await this.loadInstalledVersions()
+        } else {
+          throw new Error(response.message || '卸载失败')
+        }
+      } catch (error) {
+        if (error !== 'cancel') {
+          this.$message.error('卸载失败: ' + error.message)
+        }
+      } finally {
+        this.$set(row, 'uninstalling', false)
+      }
+    },
+    
+    // 上传前校验
+    beforeUpload(file) {
+      console.log('准备上传文件:', file.name)
+      
+      // 检查文件类型
+      const fileExt = '.' + file.name.split('.').pop().toLowerCase()
+      if (!this.allowedTypes.includes(fileExt)) {
+        this.$message.error(`不支持的文件类型:${fileExt},只支持 ${this.allowedTypes.join('、')}`)
+        return false
+      }
+      
+      // 检查文件大小
+      if (file.size > this.maxFileSize) {
+        this.$message.error(`文件大小不能超过 ${this.formatFileSize(this.maxFileSize)}`)
+        return false
+      }
+      
+      // 添加到上传进度列表
+      const uploadingFile = {
+        uid: file.uid,
+        name: file.name,
+        size: file.size,
+        percentage: 0,
+        status: 'uploading'
+      }
+      this.uploadingFiles.push(uploadingFile)
+      
+      return true
+    },
+    
+    // 上传成功回调
+    onUploadSuccess(response, file) {
+      console.log('上传成功:', file.name)
+      
+      // 更新上传进度中的文件状态
+      const uploadingFile = this.uploadingFiles.find(f => f.uid === file.uid)
+      if (uploadingFile) {
+        uploadingFile.status = 'success'
+        uploadingFile.percentage = 100
+      }
+      
+      const task = {
+        id: this.generateTaskId(),
+        fileName: file.name,
+        size: file.size,
+        status: '待安装',
+        fileId: response.data?.fileId || '',
+        url: response.data?.url || ''
+      }
+      
+      // 如果弹框打开,添加到本地数据;否则重新加载列表
+      if (this.uploadDialogVisible) {
+        this.installTasks.push(task)
+      }
+      
+      this.$message.success(`${file.name} 上传成功`)
+    },
+    
+    // 上传失败回调
+    onUploadError(error, file) {
+      console.error('上传失败:', file.name, error)
+      
+      // 更新上传进度中的文件状态
+      const uploadingFile = this.uploadingFiles.find(f => f.uid === file.uid)
+      if (uploadingFile) {
+        uploadingFile.status = 'exception'
+        uploadingFile.percentage = 0
+      }
+      
+      this.$message.error(`上传失败: ${file.name}`)
+    },
+    
+    // 上传进度回调
+    onUploadProgress(event, file) {
+      const uploadingFile = this.uploadingFiles.find(f => f.uid === file.uid)
+      if (uploadingFile) {
+        uploadingFile.percentage = Math.round((event.loaded / event.total) * 100)
+        uploadingFile.status = 'uploading'
+      }
+    },
+    
+    
+    // 全部安装
+    async installAll() {
+      console.log('批量安装所有文件')
+      console.log('当前待安装列表:', this.installTasks)
+      
+      if (!this.installTasks.length) {
+        this.$message.warning('没有待安装的文件')
+        return
+      }
+      
+      const pendingTasks = this.installTasks.filter(task => task.status === '待安装' || task.status === '失败')
+      console.log('可安装的文件:', pendingTasks)
+      
+      if (!pendingTasks.length) {
+        this.$message.warning('没有可安装的文件')
+        return
+      }
+      
+      try {
+        await this.$confirm(`确定要安装 ${pendingTasks.length} 个文件吗?`, '批量安装', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'info'
+        })
+        
+        this.installing = true
+        console.log('开始批量安装...')
+        
+        // 批量安装,由于安装成功会移除行,需要从后往前处理
+        for (let i = this.installTasks.length - 1; i >= 0; i--) {
+          const task = this.installTasks[i]
+          if (task.status === '待安装' || task.status === '失败') {
+            console.log(`安装第 ${i+1} 个文件:`, task.fileName)
+            await this.handleInstall(task, i)
+            // 安装间隔,避免并发
+            await new Promise(resolve => setTimeout(resolve, 500))
+          }
+        }
+        
+        this.$message.success('批量安装已启动')
+      } catch (error) {
+        if (error !== 'cancel') {
+          this.$message.error('批量安装出现错误')
+          console.error('批量安装错误:', error)
+      } else {
+          console.log('用户取消批量安装')
+        }
+      } finally {
+        this.installing = false
+      }
+    },
+    
+    // 清空列表
+    clearList() {
+      console.log('清空待安装列表')
+      console.log('清空前文件数量:', this.installTasks.length)
+      
+      if (this.installing) {
+        this.$message.warning('安装进行中,无法清空列表')
+        return
+      }
+      
+      // 清空所有文件
+      this.installTasks = []
+      
+      console.log('清空后文件数量:', this.installTasks.length)
+      this.$message.success('列表已清空')
+    },
+    
+    
+    // 处理安装操作
+    async handleInstall(row, index) {
+      console.log('安装文件:', row)
+      console.log('文件ID:', row.id)
+      console.log('文件名:', row.fileName)
+      console.log('当前状态:', row.status)
+      
+      try {
+        // 开始安装提示
+        this.$message.info(`开始安装 ${row.fileName}`)
+        this.installing = true
+        
+        // 模拟安装过程(2秒)
+        await new Promise((resolve) => {
+          setTimeout(() => {
+            const isSuccess = Math.random() > 0.2 // 80%成功率
+            resolve(isSuccess)
+          }, 2000)
+        }).then((isSuccess) => {
+          if (isSuccess) {
+            // 安装成功:从列表中移除该行
+            this.installTasks.splice(index, 1)
+            this.$message.success(`${row.fileName} 安装成功`)
+            // 刷新已安装版本列表
+            this.loadInstalledVersions()
+          } else {
+            // 安装失败:更新状态为失败
+            this.$set(this.installTasks, index, {
+              ...row,
+              status: '失败'
+            })
+            this.$message.error(`${row.fileName} 安装失败`)
+          }
+        })
+      } catch (error) {
+        // 安装异常:更新状态为失败
+        this.$set(this.installTasks, index, {
+          ...row,
+          status: '失败'
+        })
+        this.$message.error(`${row.fileName} 安装失败: ${error.message}`)
+      } finally {
+        this.installing = false
+      }
+    },
+    
+    // 处理删除操作
+    async handleDelete(row, index) {
+      console.log('删除文件:', row)
+      console.log('文件ID:', row.id)
+      console.log('文件名:', row.fileName)
+      
+      try {
+        await this.$confirm(`确定要删除 "${row.fileName}" 吗?`, '删除确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+        })
+        
+        // 删除操作
+        this.installTasks.splice(index, 1)
+        this.$message.success('删除成功')
+        
+        console.log('文件已删除,剩余文件数量:', this.installTasks.length)
+      } catch (error) {
+        console.log('用户取消删除')
+      }
+    },
+    
+    // 显示上传弹框
+    showUploadDialog() {
+      console.log('显示上传弹框')
+      this.uploadDialogVisible = true
+      this.uploadingFiles = []
+    },
+    
+    // 关闭上传弹框
+    closeUploadDialog() {
+      console.log('关闭上传弹框')
+      this.uploadDialogVisible = false
+      this.uploadingFiles = []
+      
+      // 重新加载待安装列表
+      this.loadUploadList()
+    },
+    
+    // 工具方法
+    generateTaskId() {
+      return 'task_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
+    },
+    
+    
+    formatFileSize(bytes) {
+      if (bytes === 0) return '0 B'
+      const k = 1024
+      const sizes = ['B', 'KB', 'MB', 'GB']
+      const i = Math.floor(Math.log(bytes) / Math.log(k))
+      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+    },
+    
+    getStatusType(status) {
+      switch (status) {
+        case '待安装': return 'info'
+        case '失败': return 'danger'
+        default: return 'info'
+      }
+    },
+    
+    getFileType(filename) {
+      const ext = filename.split('.').pop().toLowerCase()
+      switch (ext) {
+        case 'deb': return 'DEB'
+        case 'tar': return 'TAR'
+        case 'zip': return 'ZIP'
+        default: return ext.toUpperCase()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.version-management {
+  min-height: 100vh;
+  background: var(--color-bg-secondary);
+  padding-bottom: var(--spacing-6);
+
+  .page-header {
+    background: var(--color-bg-card);
+    border-bottom: 1px solid var(--color-border-primary);
+    padding: var(--spacing-6);
+    margin-bottom: var(--spacing-6);
+    box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+
+    .header-content {
+      max-width: 1200px;
+      margin: 0 auto;
+
+      .header-main {
+        .page-title {
+          color: var(--color-text-primary);
+          font-size: var(--font-size-3xl);
+          font-weight: var(--font-weight-bold);
+          margin: 0 0 var(--spacing-2) 0;
+          line-height: var(--line-height-tight);
+        }
+
+        .page-desc {
+          color: var(--color-text-secondary);
+          font-size: var(--font-size-base);
+          margin: 0 0 var(--spacing-4) 0;
+          line-height: var(--line-height-relaxed);
+        }
+      }
+    }
+
+    .search-section {
+      max-width: 1200px;
+      margin: 0 auto;
+
+      .search-input {
+        max-width: 400px;
+
+        ::v-deep .el-input__inner {
+          border-radius: var(--radius-lg);
+          border: 1px solid var(--color-border-primary);
+          background: var(--color-bg-card);
+          transition: all var(--duration-200) var(--ease-out);
+
+          &:focus {
+            border-color: var(--color-primary);
+            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+          }
+        }
+
+        ::v-deep .el-input__prefix {
+          color: var(--color-text-tertiary);
+        }
+      }
+    }
+  }
+
+  .page-content {
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 var(--spacing-6);
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-6);
+  }
+}
+
+/* 版本卡片 */
+.version-card {
+  background: var(--color-bg-card);
+  border-radius: var(--radius-xl);
+  box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
+  border: 1px solid var(--color-border-primary);
+  transition: all var(--duration-200) var(--ease-out);
+  animation: slideInUp 0.3s ease-out;
+  
+  &:hover {
+    box-shadow: 0 8px 24px rgba(2, 6, 23, 0.08);
+    border-color: var(--color-border-secondary);
+  }
+  
+  .card-header {
+    padding: var(--spacing-5);
+    border-bottom: 1px solid var(--color-border-secondary);
+    
+    .card-title {
+      font-size: var(--font-size-lg);
+      font-weight: var(--font-weight-semibold);
+      color: var(--color-text-primary);
+      margin: 0 0 var(--spacing-1) 0;
+      line-height: var(--line-height-tight);
+    }
+    
+    .card-desc {
+      font-size: var(--font-size-sm);
+      color: var(--color-text-tertiary);
+      margin: 0;
+      line-height: var(--line-height-relaxed);
+      opacity: 0.8;
+    }
+  }
+  
+  .card-content {
+    padding: var(--spacing-4) var(--spacing-5) var(--spacing-5);
+  }
+}
+
+/* 工具栏样式 */
+.install-toolbar {
+  margin-bottom: var(--spacing-4);
+  padding-bottom: var(--spacing-3);
+  border-bottom: 1px solid var(--color-border-secondary);
+  
+  .toolbar-actions {
+    display: flex;
+    gap: var(--spacing-2);
+    
+    .el-button {
+      &.el-button--success {
+        background-color: var(--color-success);
+        border-color: var(--color-success);
+        
+        &:hover {
+          background-color: var(--color-success-light);
+          border-color: var(--color-success-light);
+        }
+      }
+    }
+  }
+}
+
+/* 分页样式 */
+.pagination-wrapper {
+  display: flex;
+  justify-content: center;
+  margin-top: var(--spacing-4);
+  padding-top: var(--spacing-4);
+  border-top: 1px solid var(--color-border-secondary);
+}
+
+/* 上传弹框样式 */
+.upload-dialog {
+  ::v-deep .el-dialog__header {
+    padding: 20px 24px 16px;
+    border-bottom: 1px solid var(--color-border-secondary);
+    
+    .el-dialog__title {
+      font-size: 18px;
+      font-weight: 600;
+      color: var(--color-text-primary);
+    }
+  }
+  
+  ::v-deep .el-dialog__body {
+    padding: 24px;
+  }
+  
+  ::v-deep .el-dialog__footer {
+    padding: 16px 24px 20px;
+    border-top: 1px solid var(--color-border-secondary);
+    background: var(--color-bg-tertiary);
+  }
+}
+
+.upload-dialog-content {
+  
+  .upload-area {
+    margin-top: 0;
+    
+    .upload-container {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 16px;
+    }
+    
+    .dialog-upload-dragger {
+      ::v-deep .el-upload-dragger {
+        width: 400px;
+        height: 140px;
+        border: 2px dashed #B3D9FF;
+  border-radius: 8px;
+        background: #FFFFFF;
+        transition: all 0.3s ease;
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        
+        &:hover {
+          border-color: #409EFF;
+          box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
+        }
+        
+        .el-icon-upload {
+          font-size: 40px;
+          color: #409EFF;
+          margin-bottom: 12px;
+          transition: color 0.3s ease;
+        }
+        
+        .el-upload__text {
+          color: #666;
+          font-size: 14px;
+          text-align: center;
+          transition: color 0.3s ease;
+          
+          em {
+            color: #409EFF;
+            font-style: normal;
+            font-weight: 500;
+          }
+        }
+        
+        &:hover {
+          .el-icon-upload {
+            color: #1890FF;
+          }
+          
+          .el-upload__text {
+            color: #333;
+            
+            em {
+              color: #1890FF;
+            }
+          }
+        }
+      }
+    }
+    
+    .upload-format-hint {
+      font-size: 12px;
+      color: #999;
+      text-align: center;
+      line-height: 1.5;
+      max-width: 400px;
+    }
+  }
+  
+  .upload-progress {
+    margin-top: 20px;
+    padding-top: 16px;
+    border-top: 1px solid var(--color-border-secondary);
+    
+    h4 {
+      margin: 0 0 12px 0;
+      font-size: 14px;
+      font-weight: 600;
+      color: var(--color-text-primary);
+    }
+    
+    .upload-item {
+      margin-bottom: 12px;
+      
+      &:last-child {
+        margin-bottom: 0;
+      }
+      
+      .upload-item-info {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 6px;
+        
+        .file-name {
+          font-size: 13px;
+          color: var(--color-text-primary);
+          font-weight: 500;
+          flex: 1;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          margin-right: 12px;
+        }
+        
+        .file-size {
+          font-size: 12px;
+          color: #999;
+          flex-shrink: 0;
+        }
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  text-align: right;
+  
+  .el-button {
+    min-width: 72px;
+    
+    &.el-button--default.is-plain {
+      border-color: #D9D9D9;
+      color: #666;
+      
+      &:hover {
+        border-color: #409EFF;
+        color: #409EFF;
+        background: #F0F8FF;
+      }
+    }
+    
+    &.el-button--primary {
+      background: #409EFF;
+      border-color: #409EFF;
+      
+      &:hover {
+        background: #66B1FF;
+        border-color: #66B1FF;
+      }
+      
+      &:disabled {
+        background: #C6E2FF;
+        border-color: #C6E2FF;
+        color: #FFFFFF;
+      }
+    }
+  }
+  
+  .el-button + .el-button {
+    margin-left: 12px;
+  }
+}
+
+/* 表格容器和空状态 */
+.table-container {
+  position: relative;
+}
+
+/* 表格包装器 - 解决圆角显示问题 */
+.table-wrapper {
+  border-radius: var(--radius-lg);
+  overflow: hidden;
+  border: 1px solid var(--color-border-secondary);
+  background: var(--color-bg-tertiary);
+}
+
+.table-wrapper .el-table {
+  border: none !important;
+  border-radius: 0;
+}
+
+.empty-container {
+  height: 420px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid var(--color-border-secondary);
+  border-radius: var(--radius-lg);
+  background: var(--color-bg-secondary);
+}
+
+.xt-table {
+  ::v-deep(.el-table__header-wrapper) {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+  }
+  
+  ::v-deep(.el-table__body-wrapper) {
+    tr:hover > td {
+      background-color: #f8fafc !important;
+    }
+  }
+}
+
+.file-name-cell {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  font-size: 13px;
+  color: var(--color-text-primary);
+  padding: 2px 0;
+}
+
+/* 表格样式优化 */
+::v-deep .el-table {
+  .el-table__header-wrapper th {
+    .cell {
+      color: var(--color-text-primary);
+      font-weight: var(--font-weight-semibold);
+      padding: var(--spacing-3) var(--spacing-4);
+    }
+  }
+  
+  .el-table__body-wrapper tr:hover > td {
+    background-color: var(--color-bg-hover) !important;
+  }
+  
+  .cell {
+    padding: var(--spacing-3) var(--spacing-4);
+    white-space: nowrap;  // 防止内容换行
+    overflow: hidden;
+    text-overflow: ellipsis;
+    line-height: 1.5;
+    min-height: 20px;
+  }
+  
+  // 操作列不应用文本省略
+  .el-table__body td:last-child .cell {
+    overflow: visible;
+    text-overflow: unset;
+    white-space: nowrap;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: var(--spacing-2) var(--spacing-2);
+  }
+  
+  // 特别处理文字内容列,确保显示完整
+  .el-table__body td {
+    .cell {
+      white-space: nowrap;
+      font-variant-numeric: tabular-nums; // 数字等宽,提升对齐效果
+    }
+  }
+  
+  // 确保按钮列有足够的空间
+  .el-table__body .el-button {
+    margin: 0;
+    min-width: 42px;
+    padding: 4px 8px;
+    font-size: 12px;
+    flex-shrink: 0;
+  }
+  
+  .el-table__body .el-button + .el-button {
+    margin-left: 0;
+  }
+  
+  // 操作按钮容器
+  .action-buttons {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 8px;
+    white-space: nowrap;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    padding: 0;
+  }
+  
+  // text类型按钮样式
+  .el-table__body .el-button--text {
+    color: var(--color-primary);
+    
+    &:hover {
+      color: var(--color-primary-light);
+      background-color: rgba(64, 158, 255, 0.1);
+    }
+    
+    &:disabled {
+      color: var(--color-text-quaternary);
+      background-color: transparent;
+    }
+  }
+}
+
+/* 表格包装器内的表格特殊样式 */
+.table-wrapper {
+  ::v-deep .el-table {
+    // 设置表格背景色与容器一致,避免白角露出
+    background-color: var(--color-bg-tertiary);
+    
+    .el-table__header-wrapper th {
+      background-color: var(--color-bg-tertiary) !important;
+      
+      // 移除顶部边框,使用容器边框
+      &:first-child {
+        border-top: none;
+        border-left: none;
+      }
+      
+      &:last-child {
+        border-top: none;
+        border-right: none;
+      }
+      
+      // 其他表头单元格也移除顶部边框
+      border-top: none;
+    }
+    
+    // 确保表格主体背景
+    .el-table__body-wrapper {
+      background: var(--color-bg-card);
+      
+      tr:first-child td {
+        border-top: none;
+      }
+      
+      td:first-child {
+        border-left: none;
+      }
+      
+      td:last-child {
+        border-right: none;
+      }
+    }
+    
+    // 保留边框效果,但移除外边框
+    &.el-table--border {
+      // 移除表格外边框
+      border: none;
+      
+      // 移除伪元素边框
+      &::after {
+        display: none;
+      }
+      
+      &::before {
+        display: none;
+      }
+      
+      // 保留内部竖线和横线
+      .el-table__header-wrapper th,
+      .el-table__body-wrapper td {
+        border-right: 1px solid var(--color-border-secondary);
+        border-bottom: 1px solid var(--color-border-secondary);
+        
+        &:last-child {
+          border-right: none;
+        }
+      }
+      
+      // 最后一行移除底边框
+      .el-table__body-wrapper tr:last-child td {
+        border-bottom: none;
+      }
+    }
+  }
+}
+
+/* 状态标签样式 */
+::v-deep .el-tag {
+  border-radius: var(--radius-base);
+  font-weight: var(--font-weight-medium);
+}
+
+/* 进度条样式 */
+::v-deep .el-progress-bar {
+  .el-progress-bar__outer {
+    border-radius: var(--radius-base);
+    background-color: var(--color-bg-tertiary);
+  }
+  
+  .el-progress-bar__inner {
+    border-radius: var(--radius-base);
+  }
+}
+
+// 滑入动画
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* 响应式断点 */
+@media (max-width: 768px) {
+  .version-management {
+    .page-header {
+      padding: var(--spacing-4);
+      margin-bottom: var(--spacing-4);
+
+      .header-content {
+        .header-main {
+          .page-title {
+            font-size: var(--font-size-2xl);
+          }
+
+          .page-desc {
+            font-size: var(--font-size-sm);
+          }
+        }
+      }
+
+      .search-section {
+        .search-input {
+          max-width: 100%;
+        }
+      }
+    }
+
+    .page-content {
+      padding: 0 var(--spacing-4);
+      gap: var(--spacing-4);
+    }
+  }
+  
+  .version-card,
+  .upload-card {
+    .card-header {
+      padding: var(--spacing-4);
+    }
+    
+    .card-content {
+      padding: var(--spacing-3) var(--spacing-4) var(--spacing-4);
+    }
+  }
+  
+  .install-toolbar {
+    .toolbar-actions {
+      flex-wrap: wrap;
+      gap: var(--spacing-2);
+    }
+  }
+  
+  // 移动端表格优化
+  ::v-deep .el-table {
+    .cell {
+      padding: var(--spacing-2) var(--spacing-2);
+      font-size: 12px;
+    }
+    
+    .el-table__header-wrapper th .cell {
+      padding: var(--spacing-2) var(--spacing-2);
+      font-size: 12px;
+    }
+    
+    .el-button {
+      min-width: 40px;
+      font-size: 12px;
+      padding: 4px 8px;
+    }
+  }
+}
+
+// 暗色主题适配
+html.dark {
+  .version-management {
+    .page-header {
+      background: var(--color-bg-tertiary);
+      box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+
+      .search-section {
+        .search-input {
+          ::v-deep .el-input__inner {
+            background: var(--color-bg-quaternary);
+            border-color: var(--color-border-tertiary);
+            color: var(--color-text-primary);
+          }
+        }
+      }
+    }
+  }
+  
+  .version-card,
+  .upload-card {
+    background: var(--color-bg-tertiary);
+    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
+    border-color: var(--color-border-tertiary);
+    
+    &:hover {
+      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+      border-color: var(--color-border-secondary);
+    }
+    
+    .card-header {
+      border-bottom-color: var(--color-border-tertiary);
+      
+      .card-title {
+        color: var(--color-text-primary);
+      }
+      
+      .card-desc {
+        color: var(--color-text-quaternary);
+      }
+    }
+  }
+  
+  .install-toolbar {
+    border-bottom-color: var(--color-border-tertiary);
+  }
+  
+  .upload-dialog {
+    ::v-deep .el-dialog__header {
+      border-bottom-color: var(--color-border-tertiary);
+      
+      .el-dialog__title {
+        color: var(--color-text-primary);
+      }
+    }
+    
+    ::v-deep .el-dialog__footer {
+      border-top-color: var(--color-border-tertiary);
+      background: var(--color-bg-quaternary);
+    }
+  }
+  
+  .upload-dialog-content {
+    .upload-area {
+      .dialog-upload-dragger {
+        ::v-deep .el-upload-dragger {
+          border-color: var(--color-primary-dark);
+          background: var(--color-bg-card);
+          
+          &:hover {
+            border-color: var(--color-primary);
+            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
+          }
+          
+          .el-icon-upload {
+            color: var(--color-primary);
+          }
+          
+          .el-upload__text {
+            color: var(--color-text-secondary);
+            
+            em {
+              color: var(--color-primary);
+            }
+          }
+          
+          &:hover {
+            .el-icon-upload {
+              color: var(--color-primary-light);
+            }
+            
+            .el-upload__text {
+              color: var(--color-text-primary);
+              
+              em {
+                color: var(--color-primary-light);
+              }
+            }
+          }
+        }
+      }
+      
+      .upload-format-hint {
+        color: var(--color-text-quaternary);
+      }
+    }
+    
+    .upload-progress {
+      border-top-color: var(--color-border-tertiary);
+      
+      h4 {
+        color: var(--color-text-primary);
+      }
+      
+      .upload-item {
+        .upload-item-info {
+          .file-name {
+            color: var(--color-text-primary);
+          }
+          
+          .file-size {
+            color: var(--color-text-quaternary);
+          }
+        }
+      }
+    }
+  }
+  
+  .dialog-footer {
+    .el-button {
+      &.el-button--default.is-plain {
+        border-color: var(--color-border-tertiary);
+        color: var(--color-text-secondary);
+        
+        &:hover {
+          border-color: var(--color-primary);
+          color: var(--color-primary);
+          background: var(--color-primary-dark);
+        }
+      }
+    }
+  }
+  
+  .empty-container {
+    background: var(--color-bg-quaternary);
+    border-color: var(--color-border-tertiary);
+  }
+  
+  .table-wrapper {
+    border-color: var(--color-border-tertiary);
+    background: var(--color-bg-quaternary);
+    
+    ::v-deep .el-table {
+      background-color: var(--color-bg-quaternary);
+      
+      .el-table__header-wrapper th {
+        background-color: var(--color-bg-quaternary) !important;
+        border-color: var(--color-border-tertiary);
+      }
+      
+      .el-table__body-wrapper {
+        background: var(--color-bg-tertiary);
+        
+        td {
+          border-color: var(--color-border-tertiary);
+        }
+      }
+      
+      &.el-table--border {
+        .el-table__header-wrapper th,
+        .el-table__body-wrapper td {
+          border-right-color: var(--color-border-tertiary);
+          border-bottom-color: var(--color-border-tertiary);
+        }
+      }
+      
+      // 暗色主题下的text按钮样式
+      .el-button--text {
+        color: var(--color-primary);
+        
+        &:hover {
+          color: var(--color-primary-light);
+          background-color: rgba(64, 158, 255, 0.15);
+        }
+        
+        &:disabled {
+          color: var(--color-text-quaternary);
+          background-color: transparent;
+        }
+      }
+    }
+  }
+}
+</style>
+

+ 1 - 1
src/views/map/maplist/index.vue

@@ -995,7 +995,7 @@ export default {
           // 优先:旧路由/方法
           if (typeof this.openBuildDialog === 'function') return this.openBuildDialog(row);
           if (typeof this.constructOpen === 'boolean') {
-            this.constructOpen = true;
+          this.constructOpen = true;
             this.title = `构建地图 - ${row.name || row.mapName || ''}`;
             this.currentRow = row;
             return;