Преглед изворни кода

完善安防日志以及增加 ota、参数配置页面

yawuga пре 6 дана
родитељ
комит
a7fbd8d0a5

+ 7 - 15
src/api/base/alarmLog.js

@@ -17,28 +17,20 @@ export function getAlarmLog(id) {
   })
 }
 
-// 新增安防告警日志
-export function addAlarmLog(data) {
+// 确认告警
+export function confirmAlarmLog(id, data) {
   return request({
-    url: '/base/alarmLog',
-    method: 'post',
+    url: '/base/alarmLog/' + id + '/confirm',
+    method: 'put',
     data: data
   })
 }
 
-// 修改安防告警日志
-export function updateAlarmLog(data) {
+// 忽略告警
+export function ignoreAlarmLog(id, data) {
   return request({
-    url: '/base/alarmLog',
+    url: '/base/alarmLog/' + id + '/ignore',
     method: 'put',
     data: data
   })
 }
-
-// 删除安防告警日志
-export function delAlarmLog(id) {
-  return request({
-    url: '/base/alarmLog/' + id,
-    method: 'delete'
-  })
-}

+ 206 - 226
src/views/base/alarmLog/index.vue

@@ -1,16 +1,8 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="告警时间" prop="alarmTime">
-        <el-date-picker clearable
-          v-model="queryParams.alarmTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择告警时间">
-        </el-date-picker>
-      </el-form-item>
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
       <el-form-item label="告警类型" prop="alarmType">
-        <el-select v-model="queryParams.alarmType" placeholder="请选择告警类型" clearable>
+        <el-select v-model="queryParams.alarmType" placeholder="请选择告警类型" clearable class="search-select">
           <el-option
             v-for="dict in alarm_type"
             :key="dict.value"
@@ -20,39 +12,17 @@
         </el-select>
       </el-form-item>
       <el-form-item label="告警级别" prop="alarmLevel">
-        <el-input
-          v-model="queryParams.alarmLevel"
-          placeholder="请输入告警级别"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="告警来源" prop="alarmSource">
-        <el-input
-          v-model="queryParams.alarmSource"
-          placeholder="请输入告警来源"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="来源位置,如大厅入口、前台区域、走廊" prop="sourcePosition">
-        <el-input
-          v-model="queryParams.sourcePosition"
-          placeholder="请输入来源位置,如大厅入口、前台区域、走廊"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="告警标题或摘要" prop="alarmTitle">
-        <el-input
-          v-model="queryParams.alarmTitle"
-          placeholder="请输入告警标题或摘要"
-          clearable
-          @keyup.enter="handleQuery"
-        />
+        <el-select v-model="queryParams.alarmLevel" placeholder="请选择告警级别" clearable class="search-select">
+          <el-option
+            v-for="dict in alarm_level"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
       </el-form-item>
       <el-form-item label="处理状态" prop="handleStatus">
-        <el-select v-model="queryParams.handleStatus" placeholder="请选择处理状态" clearable>
+        <el-select v-model="queryParams.handleStatus" placeholder="请选择处理状态" clearable class="search-select">
           <el-option
             v-for="dict in alarm_handle_status"
             :key="dict.value"
@@ -61,21 +31,16 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="处理人" prop="handleBy">
-        <el-input
-          v-model="queryParams.handleBy"
-          placeholder="请输入处理人"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="处理时间" prop="handleTime">
-        <el-date-picker clearable
-          v-model="queryParams.handleTime"
-          type="date"
+      <el-form-item label="告警时间">
+        <el-date-picker
+          v-model="daterangeAlarmTime"
           value-format="YYYY-MM-DD"
-          placeholder="请选择处理时间">
-        </el-date-picker>
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          class="search-date"
+        />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -84,35 +49,6 @@
     </el-form>
 
     <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="Plus"
-          @click="handleAdd"
-          v-hasPermi="['base:alarmLog:add']"
-        >新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="Edit"
-          :disabled="single"
-          @click="handleUpdate"
-          v-hasPermi="['base:alarmLog:edit']"
-        >修改</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="danger"
-          plain
-          icon="Delete"
-          :disabled="multiple"
-          @click="handleDelete"
-          v-hasPermi="['base:alarmLog:remove']"
-        >删除</el-button>
-      </el-col>
       <el-col :span="1.5">
         <el-button
           type="warning"
@@ -125,54 +61,54 @@
       <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table v-loading="loading" :data="alarmLogList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="主键ID" align="center" prop="id" />
-      <el-table-column label="告警时间" align="center" prop="alarmTime" width="180">
+    <el-table v-loading="loading" :data="alarmLogList">
+      <el-table-column label="告警时间" align="center" prop="alarmTime" width="170">
         <template #default="scope">
-          <span>{{ parseTime(scope.row.alarmTime, '{y}-{m}-{d}') }}</span>
+          <span>{{ parseTime(scope.row.alarmTime) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="告警类型" align="center" prop="alarmType">
+      <el-table-column label="告警类型" align="center" prop="alarmType" width="110">
         <template #default="scope">
           <dict-tag :options="alarm_type" :value="scope.row.alarmType"/>
         </template>
       </el-table-column>
-      <el-table-column label="告警级别" align="center" prop="alarmLevel">
+      <el-table-column label="告警级别" align="center" prop="alarmLevel" width="100">
         <template #default="scope">
           <dict-tag :options="alarm_level" :value="scope.row.alarmLevel"/>
         </template>
       </el-table-column>
-      <el-table-column label="告警来源" align="center" prop="alarmSource">
+      <el-table-column label="告警标题" align="left" prop="alarmTitle" min-width="220" show-overflow-tooltip>
         <template #default="scope">
-          <dict-tag :options="alarm_source" :value="scope.row.alarmSource"/>
+          <span>{{ scope.row.alarmTitle || formatSummary(scope.row.description) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="来源位置,如大厅入口、前台区域、走廊" align="center" prop="sourcePosition" />
-      <el-table-column label="告警标题或摘要" align="center" prop="alarmTitle" />
-      <el-table-column label="告警描述" align="center" prop="description" />
-      <el-table-column label="抓拍图地址" align="center" prop="snapshotUrl" />
-      <el-table-column label="处理状态" align="center" prop="handleStatus">
+      <el-table-column label="来源位置" align="center" prop="sourcePosition" width="160" show-overflow-tooltip />
+      <el-table-column label="处理状态" align="center" prop="handleStatus" width="110">
         <template #default="scope">
           <dict-tag :options="alarm_handle_status" :value="scope.row.handleStatus"/>
         </template>
       </el-table-column>
-      <el-table-column label="处理人" align="center" prop="handleBy" />
-      <el-table-column label="处理时间" align="center" prop="handleTime" width="180">
+      <el-table-column label="操作" align="center" fixed="right" width="220">
         <template #default="scope">
-          <span>{{ parseTime(scope.row.handleTime, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="处理备注" align="center" prop="handleRemark" />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template #default="scope">
-          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['base:alarmLog:edit']">修改</el-button>
-          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['base:alarmLog:remove']">删除</el-button>
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
+          <el-button
+            v-if="scope.row.handleStatus === '1'"
+            link
+            type="success"
+            icon="CircleCheck"
+            @click="openHandleDialog(scope.row, 'confirm')"
+          >确认</el-button>
+          <el-button
+            v-if="scope.row.handleStatus === '1'"
+            link
+            type="warning"
+            icon="CircleClose"
+            @click="openHandleDialog(scope.row, 'ignore')"
+          >忽略</el-button>
         </template>
       </el-table-column>
     </el-table>
-    
+
     <pagination
       v-show="total>0"
       :total="total"
@@ -181,16 +117,68 @@
       @pagination="getList"
     />
 
-    <!-- 添加或修改安防告警日志对话框 -->
-    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="alarmLogRef" :model="form" :rules="rules" label-width="100px">
-        <el-row>
-        </el-row>
+    <el-dialog title="安防告警详情" v-model="detailOpen" width="820px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="告警时间">{{ parseTime(detail.alarmTime) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="告警类型">
+          <dict-tag :options="alarm_type" :value="detail.alarmType"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="告警级别">
+          <dict-tag :options="alarm_level" :value="detail.alarmLevel"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="告警来源">
+          <dict-tag :options="alarm_source" :value="detail.alarmSource"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="来源位置">{{ detail.sourcePosition || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="处理状态">
+          <dict-tag :options="alarm_handle_status" :value="detail.handleStatus"/>
+        </el-descriptions-item>
+        <el-descriptions-item label="告警标题" :span="2">{{ detail.alarmTitle || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="告警描述" :span="2">{{ detail.description || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="处理人">{{ detail.handleBy || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="处理时间">{{ parseTime(detail.handleTime) || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="处理备注" :span="2">{{ detail.handleRemark || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <div class="snapshot-block">
+        <div class="snapshot-title">抓拍图</div>
+        <el-image
+          v-if="detail.snapshotUrl"
+          class="snapshot-image"
+          :src="detail.snapshotUrl"
+          :preview-src-list="[detail.snapshotUrl]"
+          fit="cover"
+          preview-teleported
+        />
+        <div v-else class="snapshot-empty">暂无抓拍图</div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailOpen = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog :title="handleDialogTitle" v-model="handleOpen" width="520px" append-to-body>
+      <el-form ref="handleRef" :model="handleForm" label-width="90px">
+        <el-form-item label="告警标题">
+          <span>{{ handleForm.alarmTitle || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="处理备注" prop="handleRemark">
+          <el-input
+            v-model="handleForm.handleRemark"
+            type="textarea"
+            :rows="4"
+            maxlength="500"
+            show-word-limit
+            placeholder="请输入处理备注"
+          />
+        </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
+          <el-button type="primary" @click="submitHandle">确 定</el-button>
+          <el-button @click="handleOpen = false">取 消</el-button>
         </div>
       </template>
     </el-dialog>
@@ -198,83 +186,54 @@
 </template>
 
 <script setup name="AlarmLog">
-import { listAlarmLog, getAlarmLog, delAlarmLog, addAlarmLog, updateAlarmLog } from "@/api/base/alarmLog"
+import { listAlarmLog, getAlarmLog, confirmAlarmLog, ignoreAlarmLog } from "@/api/base/alarmLog"
 
 const { proxy } = getCurrentInstance()
-const { alarm_handle_status, alarm_type } = useDict('alarm_handle_status', 'alarm_type')
+const { alarm_type, alarm_level, alarm_source, alarm_handle_status } = useDict(
+  'alarm_type',
+  'alarm_level',
+  'alarm_source',
+  'alarm_handle_status'
+)
 
 const alarmLogList = ref([])
-const open = ref(false)
 const loading = ref(true)
 const showSearch = ref(true)
-const ids = ref([])
-const single = ref(true)
-const multiple = ref(true)
 const total = ref(0)
-const title = ref("")
 
-const data = reactive({
-  form: {},
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    alarmTime: undefined,
-    alarmType: undefined,
-    alarmLevel: undefined,
-    alarmSource: undefined,
-    sourcePosition: undefined,
-    alarmTitle: undefined,
-    description: undefined,
-    snapshotUrl: undefined,
-    handleStatus: undefined,
-    handleBy: undefined,
-    handleTime: undefined,
-    handleRemark: undefined,
-  },
-  rules: {
-  }
+const daterangeAlarmTime = ref([])
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  alarmType: undefined,
+  alarmLevel: undefined,
+  handleStatus: undefined
 })
 
-const { queryParams, form, rules } = toRefs(data)
+const detailOpen = ref(false)
+const detail = ref({})
+const handleOpen = ref(false)
+const handleType = ref("")
+const handleDialogTitle = ref("")
+const handleForm = reactive({
+  id: null,
+  alarmTitle: "",
+  handleRemark: ""
+})
 
 /** 查询安防告警日志列表 */
 function getList() {
   loading.value = true
-  listAlarmLog(queryParams.value).then(response => {
-    alarmLogList.value = response.rows
-    total.value = response.total
+  const params = proxy.addDateRange(queryParams.value, daterangeAlarmTime.value, 'AlarmTime')
+  listAlarmLog(params).then(response => {
+    alarmLogList.value = response.rows || []
+    total.value = response.total || 0
+  }).finally(() => {
     loading.value = false
   })
 }
 
-/** 取消按钮 */
-function cancel() {
-  open.value = false
-  reset()
-}
-
-/** 表单重置 */
-function reset() {
-  form.value = {
-    id: null,
-    alarmTime: null,
-    alarmType: null,
-    alarmLevel: null,
-    alarmSource: null,
-    sourcePosition: null,
-    alarmTitle: null,
-    description: null,
-    snapshotUrl: null,
-    handleStatus: null,
-    handleBy: null,
-    handleTime: null,
-    handleRemark: null,
-    remark: null,
-    createTime: null
-  }
-  proxy.resetForm("alarmLogRef")
-}
-
 /** 搜索按钮操作 */
 function handleQuery() {
   queryParams.value.pageNum = 1
@@ -283,73 +242,94 @@ function handleQuery() {
 
 /** 重置按钮操作 */
 function resetQuery() {
+  daterangeAlarmTime.value = []
   proxy.resetForm("queryRef")
   handleQuery()
 }
 
-/** 多选框选中数据 */
-function handleSelectionChange(selection) {
-  ids.value = selection.map(item => item.id)
-  single.value = selection.length != 1
-  multiple.value = !selection.length
-}
-
-/** 新增按钮操作 */
-function handleAdd() {
-  reset()
-  open.value = true
-  title.value = "添加安防告警日志"
+/** 详情按钮操作 */
+function handleDetail(row) {
+  getAlarmLog(row.id).then(response => {
+    detail.value = response.data || row
+    detailOpen.value = true
+  })
 }
 
-/** 修改按钮操作 */
-function handleUpdate(row) {
-  reset()
-  const _id = row.id || ids.value
-  getAlarmLog(_id).then(response => {
-    form.value = response.data
-    open.value = true
-    title.value = "修改安防告警日志"
-  })
+/** 摘要兜底方法 */
+function formatSummary(content) {
+  if (!content) return '-'
+  return content.length > 60 ? content.substring(0, 60) + '...' : content
 }
 
-/** 提交按钮 */
-function submitForm() {
-  proxy.$refs["alarmLogRef"].validate(valid => {
-    if (valid) {
-      if (form.value.id != null) {
-        updateAlarmLog(form.value).then(() => {
-          proxy.$modal.msgSuccess("修改成功")
-          open.value = false
-          getList()
-        })
-      } else {
-        addAlarmLog(form.value).then(() => {
-          proxy.$modal.msgSuccess("新增成功")
-          open.value = false
-          getList()
-        })
-      }
-    }
-  })
+/** 打开确认/忽略弹窗 */
+function openHandleDialog(row, type) {
+  handleType.value = type
+  handleDialogTitle.value = type === 'confirm' ? '确认告警' : '忽略告警'
+  handleForm.id = row.id
+  handleForm.alarmTitle = row.alarmTitle || formatSummary(row.description)
+  handleForm.handleRemark = ''
+  handleOpen.value = true
 }
 
-/** 删除按钮操作 */
-function handleDelete(row) {
-  const _ids = row.id || ids.value
-  proxy.$modal.confirm('是否确认删除安防告警日志编号为"' + _ids + '"的数据项?').then(function() {
-    return delAlarmLog(_ids)
+/** 提交确认/忽略 */
+function submitHandle() {
+  if (!handleForm.id) return
+  const request = handleType.value === 'confirm' ? confirmAlarmLog : ignoreAlarmLog
+  const successMsg = handleType.value === 'confirm' ? '告警已确认' : '告警已忽略'
+  request(handleForm.id, {
+    handleRemark: handleForm.handleRemark
   }).then(() => {
+    proxy.$modal.msgSuccess(successMsg)
+    handleOpen.value = false
     getList()
-    proxy.$modal.msgSuccess("删除成功")
-  }).catch(() => {})
+  })
 }
 
 /** 导出按钮操作 */
 function handleExport() {
-  proxy.download('base/alarmLog/export', {
-    ...queryParams.value
-  }, `alarmLog_${new Date().getTime()}.xlsx`)
+  proxy.$modal.confirm('确认导出当前查询条件下的安防告警日志数据吗?').then(() => {
+    const params = proxy.addDateRange(queryParams.value, daterangeAlarmTime.value, 'AlarmTime')
+    proxy.download('base/alarmLog/export', {
+      ...params
+    }, `安防告警日志_${new Date().getTime()}.xlsx`)
+  }).catch(() => {})
 }
 
 getList()
 </script>
+
+<style scoped>
+.search-select {
+  width: 220px;
+}
+.search-date {
+  width: 260px;
+}
+.snapshot-block {
+  margin-top: 16px;
+}
+.snapshot-title {
+  margin-bottom: 8px;
+  font-weight: 600;
+  color: #303133;
+}
+.snapshot-image {
+  width: 220px;
+  height: 140px;
+  border-radius: 6px;
+  border: 1px solid #ebeef5;
+  background: #f8fafc;
+}
+.snapshot-empty {
+  width: 220px;
+  height: 140px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #909399;
+  background: #f8fafc;
+  border: 1px dashed #dcdfe6;
+  border-radius: 6px;
+  font-size: 13px;
+}
+</style>

+ 849 - 0
src/views/base/otaUpgrade/index.vue

@@ -0,0 +1,849 @@
+<template>
+  <div class="app-container ota-upgrade-page">
+    <!-- 说明卡片 -->
+    <div class="ota-hero">
+      <div class="ota-hero-left">
+        <div class="ota-hero-title">软件版本 / OTA 升级</div>
+        <div class="ota-hero-desc">
+          当前页面为前端占位版,用于展示智能巡检机器人小车整机软件模块升级管理方式。
+          实际可升级模块、升级包格式、升级流程和进度回传方式,待机器人侧能力确认后接入。
+        </div>
+        <div class="risk-tip">
+          <el-icon><Warning /></el-icon>
+          <span>
+            智驾导航、底盘控制、驱动适配类升级可能影响机器人移动、定位、避障和底盘控制能力。
+            真实升级前必须确保机器人处于安全停靠状态。
+          </span>
+        </div>
+      </div>
+      <div class="ota-hero-right">
+        <el-tag type="info" effect="plain">前端占位</el-tag>
+      </div>
+    </div>
+
+    <!-- 当前版本信息 -->
+    <div class="ota-section-card">
+      <div class="section-header">
+        <span class="section-title">当前版本信息</span>
+        <span class="section-desc">展示机器人本地各软件模块当前版本和运行状态</span>
+      </div>
+      <div class="section-toolbar">
+        <el-button type="primary" plain icon="Refresh" @click="refreshVersions">刷新版本信息</el-button>
+      </div>
+
+      <div class="table-scroll">
+        <el-table v-loading="loading" :data="versionList" stripe class="version-table">
+          <el-table-column label="模块分类" align="center" prop="moduleCategory" width="120">
+            <template #default="scope">
+              <el-tag :type="getCategoryTagType(scope.row.moduleCategory)" effect="plain">
+                {{ formatModuleCategory(scope.row.moduleCategory) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="模块编码" align="center" prop="moduleCode" min-width="180" />
+          <el-table-column label="模块名称" align="center" prop="moduleName" min-width="140" />
+          <el-table-column label="当前版本" align="center" prop="currentVersion" width="120" />
+          <el-table-column label="运行状态" align="center" prop="runStatus" width="100">
+            <template #default="scope">
+              <el-tag :type="getRunStatusTagType(scope.row.runStatus)" effect="plain">
+                {{ formatRunStatus(scope.row.runStatus) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="安装时间" align="center" prop="installTime" width="170" />
+          <el-table-column label="备注" align="left" prop="remark" min-width="220" show-overflow-tooltip />
+        </el-table>
+      </div>
+    </div>
+
+    <!-- 升级包管理 -->
+    <div class="ota-section-card">
+      <div class="section-header">
+        <span class="section-title">升级包管理</span>
+        <span class="section-desc">展示已上传的升级包,当前为前端模拟,不执行真实上传</span>
+      </div>
+      <div class="section-toolbar">
+        <el-button type="primary" icon="Upload" @click="openUploadDialog">上传升级包</el-button>
+        <el-button plain icon="Refresh" @click="refreshPackages">刷新升级包</el-button>
+      </div>
+
+      <div class="table-scroll">
+        <el-table v-loading="loading" :data="pagedPackageList" stripe class="package-table">
+          <el-table-column label="安装包名称" align="left" prop="packageName" min-width="260" show-overflow-tooltip />
+          <el-table-column label="模块分类" align="center" prop="moduleCategory" width="120">
+            <template #default="scope">
+              <el-tag :type="getCategoryTagType(scope.row.moduleCategory)" effect="plain">
+                {{ formatModuleCategory(scope.row.moduleCategory) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="模块名称" align="center" prop="moduleName" min-width="140" />
+          <el-table-column label="目标版本" align="center" prop="targetVersion" width="120" />
+          <el-table-column label="包类型" align="center" prop="packageType" width="90">
+            <template #default="scope">
+              {{ formatPackageType(scope.row.packageType) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="文件大小" align="center" prop="fileSize" width="100" />
+          <el-table-column label="包状态" align="center" prop="packageStatus" width="90">
+            <template #default="scope">
+              <el-tag :type="getPackageStatusTagType(scope.row.packageStatus)" effect="plain">
+                {{ formatPackageStatus(scope.row.packageStatus) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="上传时间" align="center" prop="uploadTime" width="170" />
+          <el-table-column label="操作" align="center" fixed="right" width="170">
+            <template #default="scope">
+              <el-button link type="primary" icon="UploadFilled" @click="handleExecuteUpgrade(scope.row)">执行升级</el-button>
+              <el-button link type="danger" icon="Delete" @click="handleDeletePackage(scope.row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <div class="table-pagination">
+        <pagination
+          v-show="packageList.length > 0"
+          :total="packageList.length"
+          v-model:page="packagePage.pageNum"
+          v-model:limit="packagePage.pageSize"
+        />
+      </div>
+    </div>
+
+    <!-- 升级记录 -->
+    <div class="ota-section-card">
+      <div class="section-header">
+        <span class="section-title">升级记录</span>
+        <span class="section-desc">展示历史升级执行记录,当前为模拟数据</span>
+      </div>
+      <div class="section-toolbar">
+        <el-button type="primary" plain icon="Refresh" @click="refreshRecords">刷新升级记录</el-button>
+      </div>
+
+      <div class="table-scroll">
+        <el-table v-loading="loading" :data="pagedRecordList" stripe class="record-table">
+          <el-table-column label="开始时间" align="center" prop="startTime" width="170" />
+          <el-table-column label="模块分类" align="center" prop="moduleCategory" width="120">
+            <template #default="scope">
+              <el-tag :type="getCategoryTagType(scope.row.moduleCategory)" effect="plain">
+                {{ formatModuleCategory(scope.row.moduleCategory) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="模块名称" align="center" prop="moduleName" min-width="140" />
+          <el-table-column label="原版本" align="center" prop="currentVersion" width="100" />
+          <el-table-column label="目标版本" align="center" prop="targetVersion" width="100" />
+          <el-table-column label="升级状态" align="center" prop="resultStatus" width="110">
+            <template #default="scope">
+              <el-tag :type="getUpgradeStatusTagType(scope.row.resultStatus)" effect="plain">
+                {{ formatUpgradeStatus(scope.row.resultStatus) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="升级进度" align="center" prop="progressPercent" width="150">
+            <template #default="scope">
+              <div class="progress-cell">
+                <el-progress
+                  :percentage="scope.row.progressPercent || 0"
+                  :color="getProgressColor(scope.row.resultStatus)"
+                  :status="scope.row.resultStatus === '3' ? 'exception' : scope.row.resultStatus === '2' ? 'success' : undefined"
+                />
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="执行人" align="center" prop="executeBy" width="100" />
+          <el-table-column label="操作" align="center" fixed="right" width="110">
+            <template #default="scope">
+              <el-button link type="primary" icon="View" @click="handleRecordDetail(scope.row)">查看详情</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <div class="table-pagination">
+        <pagination
+          v-show="recordList.length > 0"
+          :total="recordList.length"
+          v-model:page="recordPage.pageNum"
+          v-model:limit="recordPage.pageSize"
+        />
+      </div>
+    </div>
+
+    <!-- 上传升级包弹窗 -->
+    <el-dialog title="上传升级包" v-model="uploadOpen" width="620px" append-to-body>
+      <el-form ref="uploadRef" :model="uploadForm" :rules="uploadRules" label-width="110px">
+        <el-form-item label="模块分类" prop="moduleCategory">
+          <el-select v-model="uploadForm.moduleCategory" placeholder="请选择模块分类" style="width: 100%">
+            <el-option
+              v-for="(name, key) in moduleCategoryMap"
+              :key="key"
+              :label="name"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="模块编码" prop="moduleCode">
+          <el-input v-model="uploadForm.moduleCode" placeholder="请输入模块编码" />
+        </el-form-item>
+        <el-form-item label="模块名称" prop="moduleName">
+          <el-input v-model="uploadForm.moduleName" placeholder="请输入模块名称" />
+        </el-form-item>
+        <el-form-item label="目标版本" prop="targetVersion">
+          <el-input v-model="uploadForm.targetVersion" placeholder="请输入目标版本,例如 v1.0.1" />
+        </el-form-item>
+        <el-form-item label="安装包名称" prop="packageName">
+          <el-input v-model="uploadForm.packageName" placeholder="请输入安装包文件名" />
+        </el-form-item>
+        <el-form-item label="包类型" prop="packageType">
+          <el-select v-model="uploadForm.packageType" placeholder="请选择包类型" style="width: 100%">
+            <el-option
+              v-for="(name, key) in packageTypeMap"
+              :key="key"
+              :label="name"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="安装包文件">
+          <el-upload
+            class="upload-demo"
+            drag
+            action="#"
+            :auto-upload="false"
+            :show-file-list="false"
+          >
+            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip">当前为前端模拟,不执行真实上传</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input
+            v-model="uploadForm.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注信息"
+            maxlength="200"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="uploadOpen = false">取消</el-button>
+          <el-button type="primary" @click="submitUpload">确认上传</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 升级记录详情弹窗 -->
+    <el-dialog title="升级记录详情" v-model="detailOpen" width="680px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="模块分类">
+          <el-tag :type="getCategoryTagType(detail.moduleCategory)" effect="plain">
+            {{ formatModuleCategory(detail.moduleCategory) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="模块编码">{{ detail.moduleCode || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="模块名称">{{ detail.moduleName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="原版本">{{ detail.currentVersion || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="目标版本">{{ detail.targetVersion || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="升级状态">
+          <el-tag :type="getUpgradeStatusTagType(detail.resultStatus)" effect="plain">
+            {{ formatUpgradeStatus(detail.resultStatus) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="升级进度">
+          <el-progress
+            :percentage="detail.progressPercent || 0"
+            :color="getProgressColor(detail.resultStatus)"
+            :status="detail.resultStatus === '3' ? 'exception' : detail.resultStatus === '2' ? 'success' : undefined"
+            style="width: 200px"
+          />
+        </el-descriptions-item>
+        <el-descriptions-item label="执行人">{{ detail.executeBy || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ detail.startTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ detail.endTime || (detail.resultStatus === '1' ? '升级中' : '-') }}</el-descriptions-item>
+        <el-descriptions-item label="结果信息" :span="2">{{ detail.resultMsg || '-' }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div v-if="isHighRiskModule(detail.moduleCategory)" class="detail-risk-tip">
+        <el-icon><Warning /></el-icon>
+        <span>高风险模块:该升级记录属于智驾导航、底盘控制或驱动适配相关模块。</span>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailOpen = false">关闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, getCurrentInstance } from 'vue'
+import { Warning, UploadFilled } from '@element-plus/icons-vue'
+
+const { proxy } = getCurrentInstance()
+
+const loading = ref(false)
+
+const moduleCategoryMap = {
+  '1': '业务应用',
+  '2': '安防感知',
+  '3': '智驾导航',
+  '4': '底盘控制',
+  '5': '驱动适配',
+  '9': '其他'
+}
+
+const runStatusMap = {
+  '1': '运行中',
+  '2': '已停止',
+  '3': '异常',
+  '4': '未知'
+}
+
+const packageTypeMap = {
+  '1': 'zip',
+  '2': 'tar.gz',
+  '3': 'deb',
+  '4': 'sh',
+  '5': 'apk',
+  '9': '其他'
+}
+
+const packageStatusMap = {
+  '1': '可用',
+  '0': '停用'
+}
+
+const upgradeStatusMap = {
+  '1': '升级中',
+  '2': '升级成功',
+  '3': '升级失败',
+  '4': '已取消'
+}
+
+function formatModuleCategory(value) {
+  return moduleCategoryMap[value] || '-'
+}
+
+function formatRunStatus(value) {
+  return runStatusMap[value] || '-'
+}
+
+function formatPackageType(value) {
+  return packageTypeMap[value] || '-'
+}
+
+function formatPackageStatus(value) {
+  return packageStatusMap[value] || '-'
+}
+
+function formatUpgradeStatus(value) {
+  return upgradeStatusMap[value] || '-'
+}
+
+function getCategoryTagType(category) {
+  if (category === '3' || category === '4' || category === '5') return 'warning'
+  if (category === '9') return 'info'
+  return ''
+}
+
+function getRunStatusTagType(status) {
+  const map = { '1': 'success', '2': 'info', '3': 'danger', '4': 'warning' }
+  return map[status] || 'info'
+}
+
+function getPackageStatusTagType(status) {
+  return status === '1' ? 'success' : 'info'
+}
+
+function getUpgradeStatusTagType(status) {
+  const map = { '1': 'warning', '2': 'success', '3': 'danger', '4': 'info' }
+  return map[status] || 'info'
+}
+
+function getProgressColor(status) {
+  const map = { '1': '#E6A23C', '2': '#67C23A', '3': '#F56C6C', '4': '#909399' }
+  return map[status] || '#409EFF'
+}
+
+function isHighRiskModule(category) {
+  return category === '3' || category === '4' || category === '5'
+}
+
+const versionList = ref([
+  {
+    moduleCategory: '1',
+    moduleCode: 'web-admin',
+    moduleName: '运维端后台',
+    currentVersion: 'v1.0.0',
+    runStatus: '1',
+    installTime: '2026-05-20 09:00:00',
+    remark: '本地 Web 运维后台'
+  },
+  {
+    moduleCategory: '1',
+    moduleCode: 'screen-app',
+    moduleName: '机身屏应用',
+    currentVersion: 'v1.0.0',
+    runStatus: '1',
+    installTime: '2026-05-20 09:10:00',
+    remark: '机器人 8 寸屏应用'
+  },
+  {
+    moduleCategory: '1',
+    moduleCode: 'main-service',
+    moduleName: '主控业务服务',
+    currentVersion: 'v1.0.0',
+    runStatus: '1',
+    installTime: '2026-05-20 09:20:00',
+    remark: '本地业务主服务'
+  },
+  {
+    moduleCategory: '2',
+    moduleCode: 'vision-service',
+    moduleName: '视觉识别服务',
+    currentVersion: 'v0.9.3',
+    runStatus: '1',
+    installTime: '2026-05-18 10:00:00',
+    remark: '摄像头识别和安防告警能力'
+  },
+  {
+    moduleCategory: '3',
+    moduleCode: 'navigation-service',
+    moduleName: '导航服务',
+    currentVersion: 'v0.8.5',
+    runStatus: '1',
+    installTime: '2026-05-18 10:30:00',
+    remark: '定位、路径规划、导航控制相关服务'
+  },
+  {
+    moduleCategory: '4',
+    moduleCode: 'chassis-service',
+    moduleName: '底盘控制服务',
+    currentVersion: 'v0.7.2',
+    runStatus: '1',
+    installTime: '2026-05-18 11:00:00',
+    remark: '底盘控制、运动控制相关服务'
+  },
+  {
+    moduleCategory: '5',
+    moduleCode: 'lidar-driver',
+    moduleName: '激光雷达驱动',
+    currentVersion: 'v0.6.1',
+    runStatus: '4',
+    installTime: '2026-05-18 11:30:00',
+    remark: '雷达数据采集和驱动适配'
+  }
+])
+
+const packageList = ref([
+  {
+    id: 1,
+    packageName: 'screen-app-v1.0.1.zip',
+    moduleCategory: '1',
+    moduleCode: 'screen-app',
+    moduleName: '机身屏应用',
+    targetVersion: 'v1.0.1',
+    packageType: '1',
+    fileSize: '36.5MB',
+    packageStatus: '1',
+    uploadBy: 'admin',
+    uploadTime: '2026-05-20 10:00:00',
+    remark: '屏幕端显示优化包'
+  },
+  {
+    id: 2,
+    packageName: 'navigation-service-v0.8.6.tar.gz',
+    moduleCategory: '3',
+    moduleCode: 'navigation-service',
+    moduleName: '导航服务',
+    targetVersion: 'v0.8.6',
+    packageType: '2',
+    fileSize: '128.4MB',
+    packageStatus: '1',
+    uploadBy: 'admin',
+    uploadTime: '2026-05-20 10:20:00',
+    remark: '智驾导航服务升级包'
+  },
+  {
+    id: 3,
+    packageName: 'chassis-service-v0.7.3.deb',
+    moduleCategory: '4',
+    moduleCode: 'chassis-service',
+    moduleName: '底盘控制服务',
+    targetVersion: 'v0.7.3',
+    packageType: '3',
+    fileSize: '82.7MB',
+    packageStatus: '1',
+    uploadBy: 'admin',
+    uploadTime: '2026-05-20 10:40:00',
+    remark: '底盘控制服务升级包'
+  },
+  {
+    id: 4,
+    packageName: 'lidar-driver-v0.6.2.sh',
+    moduleCategory: '5',
+    moduleCode: 'lidar-driver',
+    moduleName: '激光雷达驱动',
+    targetVersion: 'v0.6.2',
+    packageType: '4',
+    fileSize: '18.2MB',
+    packageStatus: '0',
+    uploadBy: 'admin',
+    uploadTime: '2026-05-19 16:30:00',
+    remark: '驱动适配测试包'
+  }
+])
+
+const recordList = ref([
+  {
+    id: 1,
+    moduleCategory: '1',
+    moduleCode: 'screen-app',
+    moduleName: '机身屏应用',
+    currentVersion: 'v1.0.0',
+    targetVersion: 'v1.0.1',
+    startTime: '2026-05-20 11:00:00',
+    endTime: '2026-05-20 11:03:20',
+    resultStatus: '2',
+    progressPercent: 100,
+    executeBy: 'admin',
+    resultMsg: '升级成功'
+  },
+  {
+    id: 2,
+    moduleCategory: '3',
+    moduleCode: 'navigation-service',
+    moduleName: '导航服务',
+    currentVersion: 'v0.8.5',
+    targetVersion: 'v0.8.6',
+    startTime: '2026-05-20 11:20:00',
+    endTime: '',
+    resultStatus: '1',
+    progressPercent: 65,
+    executeBy: 'admin',
+    resultMsg: '升级执行中,等待机器人侧回传结果'
+  },
+  {
+    id: 3,
+    moduleCategory: '4',
+    moduleCode: 'chassis-service',
+    moduleName: '底盘控制服务',
+    currentVersion: 'v0.7.1',
+    targetVersion: 'v0.7.2',
+    startTime: '2026-05-19 15:00:00',
+    endTime: '2026-05-19 15:06:40',
+    resultStatus: '3',
+    progressPercent: 48,
+    executeBy: 'admin',
+    resultMsg: '升级失败:底盘服务重启后未在规定时间内恢复心跳'
+  }
+])
+
+const uploadOpen = ref(false)
+const uploadRef = ref()
+
+const uploadForm = reactive({
+  moduleCategory: '',
+  moduleCode: '',
+  moduleName: '',
+  targetVersion: '',
+  packageName: '',
+  packageType: '',
+  remark: ''
+})
+
+const uploadRules = {
+  moduleCategory: [{ required: true, message: '请选择模块分类', trigger: 'change' }],
+  moduleCode: [{ required: true, message: '请输入模块编码', trigger: 'blur' }],
+  moduleName: [{ required: true, message: '请输入模块名称', trigger: 'blur' }],
+  targetVersion: [{ required: true, message: '请输入目标版本', trigger: 'blur' }],
+  packageName: [{ required: true, message: '请输入安装包名称', trigger: 'blur' }],
+  packageType: [{ required: true, message: '请选择包类型', trigger: 'change' }]
+}
+
+const packagePage = reactive({
+  pageNum: 1,
+  pageSize: 5
+})
+
+const pagedPackageList = computed(() => {
+  const start = (packagePage.pageNum - 1) * packagePage.pageSize
+  const end = start + packagePage.pageSize
+  return packageList.value.slice(start, end)
+})
+
+const recordPage = reactive({
+  pageNum: 1,
+  pageSize: 5
+})
+
+const pagedRecordList = computed(() => {
+  const start = (recordPage.pageNum - 1) * recordPage.pageSize
+  const end = start + recordPage.pageSize
+  return recordList.value.slice(start, end)
+})
+
+const detailOpen = ref(false)
+const detail = ref({})
+
+function getNowTime() {
+  const date = new Date()
+  const pad = value => String(value).padStart(2, '0')
+  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+}
+
+function refreshVersions() {
+  proxy.$modal.msgWarning('版本信息接口暂未接入,当前为前端模拟数据')
+}
+
+function refreshPackages() {
+  proxy.$modal.msgWarning('升级包列表接口暂未接入,当前为前端模拟数据')
+}
+
+function refreshRecords() {
+  proxy.$modal.msgWarning('升级记录接口暂未接入,当前为前端模拟数据')
+}
+
+function openUploadDialog() {
+  uploadForm.moduleCategory = ''
+  uploadForm.moduleCode = ''
+  uploadForm.moduleName = ''
+  uploadForm.targetVersion = ''
+  uploadForm.packageName = ''
+  uploadForm.packageType = ''
+  uploadForm.remark = ''
+  uploadOpen.value = true
+}
+
+function submitUpload() {
+  uploadRef.value.validate(valid => {
+    if (!valid) return
+
+    packageList.value.unshift({
+      id: Date.now(),
+      packageName: uploadForm.packageName,
+      moduleCategory: uploadForm.moduleCategory,
+      moduleCode: uploadForm.moduleCode,
+      moduleName: uploadForm.moduleName,
+      targetVersion: uploadForm.targetVersion,
+      packageType: uploadForm.packageType,
+      fileSize: '模拟文件',
+      packageStatus: '1',
+      uploadBy: 'admin',
+      uploadTime: getNowTime(),
+      remark: uploadForm.remark || '前端模拟上传记录'
+    })
+
+    packagePage.pageNum = 1
+    uploadOpen.value = false
+    proxy.$modal.msgSuccess('升级包上传接口暂未接入,当前仅临时加入前端列表演示')
+  })
+}
+
+function handleExecuteUpgrade(row) {
+  if (row.packageStatus !== '1') {
+    proxy.$modal.msgError('当前升级包不可用')
+    return
+  }
+
+  if (isHighRiskModule(row.moduleCategory)) {
+    proxy.$modal.confirm(
+      '当前升级对象属于智驾导航、底盘控制或驱动适配相关模块,升级过程可能影响机器人移动、定位、避障或底盘控制能力。请确认机器人处于安全停靠状态后再执行升级。',
+      '高风险升级确认',
+      {
+        confirmButtonText: '确认升级',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    ).then(() => {
+      appendMockUpgradeRecord(row)
+      proxy.$modal.msgWarning('升级执行接口暂未接入,当前仅新增前端模拟升级记录')
+    }).catch(() => {})
+  } else {
+    proxy.$modal.confirm('确认使用该安装包执行升级吗?').then(() => {
+      appendMockUpgradeRecord(row)
+      proxy.$modal.msgWarning('升级执行接口暂未接入,当前仅新增前端模拟升级记录')
+    }).catch(() => {})
+  }
+}
+
+function appendMockUpgradeRecord(row) {
+  recordList.value.unshift({
+    id: Date.now(),
+    moduleCategory: row.moduleCategory,
+    moduleCode: row.moduleCode,
+    moduleName: row.moduleName,
+    currentVersion: '-',
+    targetVersion: row.targetVersion,
+    startTime: getNowTime(),
+    endTime: '',
+    resultStatus: '1',
+    progressPercent: 10,
+    executeBy: 'admin',
+    resultMsg: '升级执行接口暂未接入,当前为前端模拟升级记录'
+  })
+
+  recordPage.pageNum = 1
+}
+
+function handleDeletePackage(row) {
+  proxy.$modal.confirm('确认删除该升级包记录吗?').then(() => {
+    packageList.value = packageList.value.filter(item => item.id !== row.id)
+
+    if (pagedPackageList.value.length === 0 && packagePage.pageNum > 1) {
+      packagePage.pageNum -= 1
+    }
+
+    proxy.$modal.msgSuccess('升级包删除接口暂未接入,当前仅从前端列表临时移除')
+  }).catch(() => {})
+}
+
+function handleRecordDetail(row) {
+  detail.value = { ...row }
+  detailOpen.value = true
+}
+</script>
+
+<style scoped>
+.ota-upgrade-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  padding-bottom: 24px;
+}
+
+.ota-hero {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: #ffffff;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+}
+
+.ota-hero-title {
+  margin-bottom: 6px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.ota-hero-desc {
+  margin-bottom: 10px;
+  color: #909399;
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.ota-hero-right {
+  flex-shrink: 0;
+  margin-left: 16px;
+  padding-top: 2px;
+}
+
+.risk-tip {
+  display: flex;
+  align-items: flex-start;
+  gap: 6px;
+  padding: 10px 12px;
+  color: #e6a23c;
+  font-size: 13px;
+  line-height: 1.5;
+  background: #fdf6ec;
+  border: 1px solid #f5dab1;
+  border-radius: 4px;
+}
+
+.risk-tip .el-icon {
+  flex-shrink: 0;
+  margin-top: 1px;
+}
+
+.ota-section-card {
+  padding: 16px 20px;
+  background: #ffffff;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+
+.section-header {
+  display: flex;
+  align-items: baseline;
+  gap: 10px;
+  margin-bottom: 14px;
+}
+
+.section-title {
+  color: #303133;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.section-desc {
+  color: #909399;
+  font-size: 13px;
+}
+
+.section-toolbar {
+  margin-bottom: 12px;
+}
+
+.table-scroll {
+  width: 100%;
+  overflow-x: auto;
+}
+
+.version-table {
+  min-width: 1100px;
+}
+
+.package-table,
+.record-table {
+  min-width: 1200px;
+}
+
+.table-pagination {
+  margin-top: 12px;
+}
+
+.progress-cell {
+  display: flex;
+  align-items: center;
+}
+
+.detail-risk-tip {
+  display: flex;
+  align-items: flex-start;
+  gap: 6px;
+  margin-top: 14px;
+  padding: 10px 12px;
+  color: #e6a23c;
+  font-size: 13px;
+  line-height: 1.5;
+  background: #fdf6ec;
+  border: 1px solid #f5dab1;
+  border-radius: 4px;
+}
+
+.detail-risk-tip .el-icon {
+  flex-shrink: 0;
+  margin-top: 1px;
+}
+
+.upload-demo {
+  width: 100%;
+}
+</style>

+ 642 - 0
src/views/base/paramConfig/index.vue

@@ -0,0 +1,642 @@
+<template>
+  <div class="app-container param-config-page">
+    <!-- 顶部说明卡片 -->
+    <div class="param-hero">
+      <div class="param-hero-left">
+        <div class="param-hero-title">运行参数配置</div>
+        <div class="param-hero-desc">
+          当前页面为前端占位版,用于展示后续机器人运行参数配置方式。
+          参数保存、恢复默认和下发机器人能力待后端及机器人侧接口确认后接入。
+        </div>
+      </div>
+      <div class="param-hero-right">
+        <el-tag type="info" effect="plain">前端占位</el-tag>
+      </div>
+    </div>
+
+    <!-- 参数分组 Tab -->
+    <div class="param-tabs-card">
+      <el-tabs v-model="activeGroup" @tab-change="handleTabChange">
+        <el-tab-pane
+          v-for="group in paramGroups"
+          :key="group.code"
+          :label="group.name"
+          :name="group.code"
+        />
+      </el-tabs>
+
+      <!-- 当前分组说明 -->
+      <div class="group-summary">
+        <span class="group-summary-name">{{ currentGroup.name }}</span>
+        <span class="group-summary-sep">—</span>
+        <span class="group-summary-desc">{{ currentGroup.desc }}</span>
+        <span class="group-summary-count">(共 {{ currentParams.length }} 个参数)</span>
+      </div>
+
+      <!-- 当前分组参数配置表单 -->
+      <div class="param-grid">
+        <div
+          v-for="param in currentParams"
+          :key="param.paramCode"
+          class="param-card"
+        >
+          <div class="param-card-header">
+            <div class="param-card-title">
+              <span class="param-name">{{ param.paramName }}</span>
+              <el-tag v-if="param.required" type="danger" size="small" effect="plain">必填</el-tag>
+            </div>
+            <div class="param-card-status">
+              <el-tag :type="param.editable ? 'success' : 'warning'" size="small" effect="plain">
+                {{ param.editable ? '可编辑' : '只读' }}
+              </el-tag>
+            </div>
+          </div>
+
+          <div class="param-card-code">{{ param.paramCode }}</div>
+
+          <div class="param-card-control">
+            <!-- string -->
+            <el-input
+              v-if="param.valueType === 'string'"
+              v-model="param.paramValue"
+              :disabled="!param.editable"
+              :placeholder="param.remark"
+              clearable
+            />
+            <!-- int -->
+            <el-input-number
+              v-else-if="param.valueType === 'int'"
+              v-model="param.paramValue"
+              :disabled="!param.editable"
+              :min="param.min"
+              :max="param.max"
+              :precision="0"
+              controls-position="right"
+              style="width: 100%"
+            />
+            <!-- float -->
+            <el-input-number
+              v-else-if="param.valueType === 'float'"
+              v-model="param.paramValue"
+              :disabled="!param.editable"
+              :min="param.min"
+              :max="param.max"
+              :precision="2"
+              controls-position="right"
+              style="width: 100%"
+            />
+            <!-- boolean -->
+            <el-switch
+              v-else-if="param.valueType === 'boolean'"
+              v-model="param.paramValue"
+              :disabled="!param.editable"
+              inline-prompt
+              active-text="开"
+              inactive-text="关"
+            />
+            <!-- enum -->
+            <el-select
+              v-else-if="param.valueType === 'enum'"
+              v-model="param.paramValue"
+              :disabled="!param.editable"
+              placeholder="请选择"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="opt in param.options"
+                :key="opt.value"
+                :label="opt.label"
+                :value="opt.value"
+              />
+            </el-select>
+          </div>
+
+          <div class="param-card-meta">
+            <span v-if="param.unit" class="param-unit">单位:{{ param.unit }}</span>
+            <span class="param-default">默认值:{{ param.defaultValue }}</span>
+            <span v-if="param.min !== undefined && param.max !== undefined" class="param-range">
+              范围:{{ param.min }}~{{ param.max }}
+            </span>
+          </div>
+
+          <div class="param-card-remark">{{ param.remark }}</div>
+        </div>
+      </div>
+
+      <!-- 底部操作按钮 -->
+      <div class="param-actions">
+        <el-button @click="refreshParams">刷新</el-button>
+        <el-button type="warning" plain @click="resetCurrentGroup">恢复当前分组默认值</el-button>
+        <el-button type="primary" @click="saveCurrentGroup">保存当前分组</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, getCurrentInstance } from 'vue'
+
+const { proxy } = getCurrentInstance()
+
+const activeGroup = ref('audio')
+
+const paramGroups = [
+  { code: 'audio', name: '音频参数', desc: '扬声器、麦克风、远程喊话相关参数' },
+  { code: 'screen', name: '屏幕参数', desc: '亮度、待机、触摸交互相关参数' },
+  { code: 'network', name: '网络参数', desc: '本地服务、视频流、网络连接相关参数' },
+  { code: 'device', name: '设备参数', desc: '电量、充电、设备运行相关参数' },
+  { code: 'service', name: '服务参数', desc: '问答、播报、视频、TTS 等服务参数' },
+  { code: 'system', name: '系统参数', desc: '日志、存储、系统维护相关参数' }
+]
+
+const mockParams = reactive({
+  audio: [
+    {
+      paramCode: 'speaker_volume',
+      paramName: '扬声器音量',
+      paramValue: 70,
+      defaultValue: 70,
+      valueType: 'int',
+      unit: '%',
+      editable: true,
+      required: true,
+      min: 0,
+      max: 100,
+      remark: '控制机器人扬声器播放音量'
+    },
+    {
+      paramCode: 'mic_gain',
+      paramName: '麦克风增益',
+      paramValue: 60,
+      defaultValue: 60,
+      valueType: 'int',
+      unit: '%',
+      editable: true,
+      required: true,
+      min: 0,
+      max: 100,
+      remark: '调整麦克风采集灵敏度'
+    },
+    {
+      paramCode: 'shout_volume',
+      paramName: '远程喊话音量',
+      paramValue: 80,
+      defaultValue: 80,
+      valueType: 'int',
+      unit: '%',
+      editable: true,
+      required: false,
+      min: 0,
+      max: 100,
+      remark: '远程喊话时的扬声器输出音量'
+    },
+    {
+      paramCode: 'broadcast_enabled',
+      paramName: '启用播报',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '关闭后将不执行语音播报'
+    }
+  ],
+  screen: [
+    {
+      paramCode: 'screen_brightness',
+      paramName: '屏幕亮度',
+      paramValue: 80,
+      defaultValue: 80,
+      valueType: 'int',
+      unit: '%',
+      editable: true,
+      required: true,
+      min: 0,
+      max: 100,
+      remark: '调整机器人屏幕显示亮度'
+    },
+    {
+      paramCode: 'standby_timeout',
+      paramName: '待机超时时间',
+      paramValue: 300,
+      defaultValue: 300,
+      valueType: 'int',
+      unit: '秒',
+      editable: true,
+      required: false,
+      min: 10,
+      max: 3600,
+      remark: '无操作后进入待机页的时间'
+    },
+    {
+      paramCode: 'touch_wakeup_enabled',
+      paramName: '触摸唤醒',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '待机时触摸屏幕是否唤醒服务页'
+    },
+    {
+      paramCode: 'theme_mode',
+      paramName: '主题模式',
+      paramValue: '1',
+      defaultValue: '1',
+      valueType: 'enum',
+      editable: true,
+      required: false,
+      options: [
+        { value: '1', label: '默认' },
+        { value: '2', label: '简洁' },
+        { value: '3', label: '高对比' }
+      ],
+      remark: '屏幕显示主题模式'
+    }
+  ],
+  network: [
+    {
+      paramCode: 'local_service_port',
+      paramName: '本地服务端口',
+      paramValue: 8080,
+      defaultValue: 8080,
+      valueType: 'int',
+      editable: true,
+      required: true,
+      min: 1,
+      max: 65535,
+      remark: '本地后台服务监听端口'
+    },
+    {
+      paramCode: 'video_stream_url',
+      paramName: '视频流地址',
+      paramValue: 'rtsp://192.168.1.100:554/stream',
+      defaultValue: 'rtsp://192.168.1.100:554/stream',
+      valueType: 'string',
+      editable: true,
+      required: false,
+      remark: '机器人摄像头视频流地址'
+    },
+    {
+      paramCode: 'network_check_enabled',
+      paramName: '启用网络检测',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '定期检测网络连通状态'
+    },
+    {
+      paramCode: 'network_check_interval',
+      paramName: '网络检测间隔',
+      paramValue: 60,
+      defaultValue: 60,
+      valueType: 'int',
+      unit: '秒',
+      editable: true,
+      required: false,
+      min: 10,
+      max: 3600,
+      remark: '两次网络检测之间的间隔'
+    }
+  ],
+  device: [
+    {
+      paramCode: 'low_battery_threshold',
+      paramName: '低电量阈值',
+      paramValue: 20,
+      defaultValue: 20,
+      valueType: 'int',
+      unit: '%',
+      editable: true,
+      required: true,
+      min: 5,
+      max: 50,
+      remark: '低于该值时触发低电量告警'
+    },
+    {
+      paramCode: 'auto_charge_enabled',
+      paramName: '启用自动回充',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '低电量时自动返回充电桩'
+    },
+    {
+      paramCode: 'max_work_temperature',
+      paramName: '最高工作温度',
+      paramValue: 45.0,
+      defaultValue: 45.0,
+      valueType: 'float',
+      unit: '℃',
+      editable: true,
+      required: false,
+      min: 0,
+      max: 80,
+      remark: '机器人温度超过该值时触发告警'
+    },
+    {
+      paramCode: 'device_mode',
+      paramName: '设备运行模式',
+      paramValue: '1',
+      defaultValue: '1',
+      valueType: 'enum',
+      editable: true,
+      required: false,
+      options: [
+        { value: '1', label: '普通' },
+        { value: '2', label: '安防' },
+        { value: '3', label: '接待' }
+      ],
+      remark: '机器人当前运行场景模式'
+    }
+  ],
+  service: [
+    {
+      paramCode: 'qa_service_enabled',
+      paramName: '启用问答服务',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '关闭后机器人不响应问答服务'
+    },
+    {
+      paramCode: 'tts_service_enabled',
+      paramName: '启用 TTS 服务',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '关闭后不进行语音合成播报'
+    },
+    {
+      paramCode: 'tts_voice_type',
+      paramName: 'TTS 音色',
+      paramValue: '1',
+      defaultValue: '1',
+      valueType: 'enum',
+      editable: true,
+      required: false,
+      options: [
+        { value: '1', label: '标准女声' },
+        { value: '2', label: '标准男声' },
+        { value: '3', label: '亲和女声' }
+      ],
+      remark: 'TTS 语音合成音色选择'
+    },
+    {
+      paramCode: 'video_service_enabled',
+      paramName: '启用视频服务',
+      paramValue: true,
+      defaultValue: true,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '关闭后不提供视频预览'
+    }
+  ],
+  system: [
+    {
+      paramCode: 'log_level',
+      paramName: '日志级别',
+      paramValue: '2',
+      defaultValue: '2',
+      valueType: 'enum',
+      editable: true,
+      required: true,
+      options: [
+        { value: '1', label: 'DEBUG' },
+        { value: '2', label: 'INFO' },
+        { value: '3', label: 'WARN' },
+        { value: '4', label: 'ERROR' }
+      ],
+      remark: '系统日志记录的最低级别'
+    },
+    {
+      paramCode: 'log_retention_days',
+      paramName: '日志保留天数',
+      paramValue: 180,
+      defaultValue: 180,
+      valueType: 'int',
+      unit: '天',
+      editable: true,
+      required: false,
+      min: 1,
+      max: 365,
+      remark: '本地运行日志保留天数'
+    },
+    {
+      paramCode: 'storage_warning_threshold',
+      paramName: '存储告警阈值',
+      paramValue: 80,
+      defaultValue: 80,
+      valueType: 'int',
+      unit: '%',
+      editable: true,
+      required: false,
+      min: 50,
+      max: 95,
+      remark: '存储使用率超过该阈值时触发告警'
+    },
+    {
+      paramCode: 'auto_cleanup_enabled',
+      paramName: '启用自动清理',
+      paramValue: false,
+      defaultValue: false,
+      valueType: 'boolean',
+      editable: true,
+      required: false,
+      remark: '是否自动清理过期日志和缓存'
+    }
+  ]
+})
+
+const currentGroup = computed(() => paramGroups.find(item => item.code === activeGroup.value) || paramGroups[0])
+const currentParams = computed(() => mockParams[activeGroup.value] || [])
+
+function handleTabChange() {
+  // Tab 切换时无需额外处理,computed 会自动更新
+}
+
+function refreshParams() {
+  proxy.$modal.msgWarning('参数接口暂未接入,当前为前端模拟数据')
+}
+
+function resetCurrentGroup() {
+  proxy.$modal.confirm('确认恢复当前分组参数为默认值吗?').then(() => {
+    const params = currentParams.value
+    for (const item of params) {
+      item.paramValue = item.defaultValue
+    }
+    proxy.$modal.msgSuccess('已恢复当前分组默认值,保存接口暂未接入')
+  }).catch(() => {})
+}
+
+function validateCurrentGroup() {
+  for (const item of currentParams.value) {
+    if (item.required && (item.paramValue === '' || item.paramValue === null || item.paramValue === undefined)) {
+      proxy.$modal.msgError(`${item.paramName}不能为空`)
+      return false
+    }
+    if ((item.valueType === 'int' || item.valueType === 'float') && item.paramValue !== '' && item.paramValue !== null && item.paramValue !== undefined) {
+      const value = Number(item.paramValue)
+      if (Number.isNaN(value)) {
+        proxy.$modal.msgError(`${item.paramName}必须为数字`)
+        return false
+      }
+      if (item.min !== undefined && value < item.min) {
+        proxy.$modal.msgError(`${item.paramName}不能小于${item.min}`)
+        return false
+      }
+      if (item.max !== undefined && value > item.max) {
+        proxy.$modal.msgError(`${item.paramName}不能大于${item.max}`)
+        return false
+      }
+    }
+  }
+  return true
+}
+
+function saveCurrentGroup() {
+  if (!validateCurrentGroup()) return
+  proxy.$modal.msgSuccess('参数保存接口暂未接入,当前仅完成前端配置演示')
+}
+</script>
+
+<style scoped>
+.param-config-page {
+  padding-bottom: 24px;
+}
+
+/* 顶部说明卡片 */
+.param-hero {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: #ffffff;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  margin-bottom: 16px;
+}
+.param-hero-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 6px;
+}
+.param-hero-desc {
+  font-size: 13px;
+  color: #909399;
+  line-height: 1.6;
+}
+.param-hero-right {
+  flex-shrink: 0;
+  margin-left: 16px;
+  padding-top: 2px;
+}
+
+/* Tab 卡片 */
+.param-tabs-card {
+  background: #ffffff;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  padding: 0 20px 20px;
+}
+
+/* 分组说明 */
+.group-summary {
+  font-size: 13px;
+  color: #606266;
+  margin-bottom: 16px;
+  padding: 10px 0 0;
+  border-top: 1px solid #f0f0f0;
+}
+.group-summary-name {
+  font-weight: 600;
+  color: #303133;
+}
+.group-summary-sep {
+  margin: 0 6px;
+  color: #c0c4cc;
+}
+.group-summary-count {
+  color: #909399;
+  margin-left: 4px;
+}
+
+/* 参数卡片网格 */
+.param-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+  margin-bottom: 20px;
+}
+
+@media (max-width: 1200px) {
+  .param-grid {
+    grid-template-columns: 1fr;
+  }
+}
+
+/* 参数卡片 */
+.param-card {
+  background: #fafafa;
+  border: 1px solid #f0f0f0;
+  border-radius: 6px;
+  padding: 14px 16px;
+}
+.param-card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 6px;
+}
+.param-card-title {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.param-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+.param-card-code {
+  font-size: 12px;
+  color: #909399;
+  font-family: 'Courier New', monospace;
+  margin-bottom: 10px;
+}
+.param-card-control {
+  margin-bottom: 8px;
+}
+.param-card-meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px 12px;
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 6px;
+}
+.param-card-remark {
+  font-size: 12px;
+  color: #c0c4cc;
+  line-height: 1.5;
+}
+
+/* 底部操作按钮 */
+.param-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding-top: 16px;
+  border-top: 1px solid #f0f0f0;
+}
+</style>