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

feat:【IoT 物联网】初始化 IoT 固件详情页 50%

YunaiV 7 месяцев назад
Родитель
Сommit
4cecafb4b1

+ 1 - 0
src/api/iot/ota/firmware/index.ts

@@ -7,6 +7,7 @@ export interface IoTOtaFirmware {
   description?: string // 固件描述
   version?: string // 版本号
   productId?: number // 产品编号
+  productName?: string // 产品名称
   fileUrl?: string // 固件文件 URL
   fileSize?: number // 固件文件大小
   fileDigestAlgorithm?: string // 固件文件签名算法

+ 3 - 1
src/utils/dict.ts

@@ -241,5 +241,7 @@ export enum DICT_TYPE {
   IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举
   IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum', // IoT 规则场景的触发类型枚举
   IOT_ALERT_LEVEL = 'iot_alert_level', // IoT 告警级别
-  IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type' // IoT 告警接收类型
+  IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type', // IoT 告警接收类型
+  IOT_OTA_TASK_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA任务设备范围
+  IOT_OTA_TASK_STATUS = 'iot_ota_task_status' // IoT OTA任务状态
 }

+ 86 - 408
src/views/iot/ota/firmware/detail/index.vue

@@ -1,303 +1,124 @@
 <template>
   <div class="app-container">
     <!-- 固件信息 -->
-    <!-- TODO @AI:可以使用 ELDescription 原生的组件么? -->
     <ContentWrap title="固件信息" class="mb-20px">
-      <el-row :gutter="20" v-loading="firmwareLoading">
-        <el-col :span="8">
-          <div class="info-item">
-            <span class="label">固件名称:</span>
-            <span class="value">{{ firmwareInfo.name || '-' }}</span>
+      <el-descriptions :column="3" v-loading="firmwareLoading">
+        <el-descriptions-item label="固件名称">
+          {{ firmware?.name }}
+        </el-descriptions-item>
+        <el-descriptions-item label="所属产品">
+          {{ firmware?.productName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="固件版本">
+          {{ firmware?.version }}
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间">
+          {{ firmware?.createTime ? formatDate(firmware.createTime) : '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="固件描述" :span="2">
+          {{ firmware?.description }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </ContentWrap>
+
+    <!-- 固件升级设备统计 -->
+    <ContentWrap title="固件升级设备统计" class="mb-20px">
+      <el-row :gutter="20" class="py-20px" v-loading="statisticsLoading">
+        <el-col :span="6">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-blue-500">{{ statistics.total || 0 }}</div>
+            <div class="text-14px text-gray-600">升级设备总数</div>
           </div>
         </el-col>
-        <el-col :span="8">
-          <div class="info-item">
-            <span class="label">所属产品:</span>
-            <span class="value">{{ productName || '-' }}</span>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-gray-400">
+              {{ statistics.pending || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">待推送</div>
           </div>
         </el-col>
-        <!-- TODO @AI:移除 -->
-        <el-col :span="8">
-          <div class="info-item">
-            <span class="label">是否最新:</span>
-            <span class="value">{{ isLatest ? '是' : '否' }}</span>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-blue-400">{{ statistics.pushed || 0 }}</div>
+            <div class="text-14px text-gray-600">已推送</div>
           </div>
         </el-col>
-      </el-row>
-      <el-row :gutter="20" class="mt-10px">
-        <!-- TODO @AI:移除 -->
-        <el-col :span="8">
-          <div class="info-item">
-            <span class="label">固件类型:</span>
-            <span class="value">{{ firmwareInfo.fileDigestAlgorithm || 'http' }}</span>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-yellow-500">
+              {{ statistics.inProgress || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">正在升级</div>
           </div>
         </el-col>
-        <el-col :span="8">
-          <div class="info-item">
-            <span class="label">固件版本:</span>
-            <span class="value">{{ firmwareInfo.version || 'Version 1' }}</span>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-green-500">
+              {{ statistics.success || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级成功</div>
           </div>
         </el-col>
-        <el-col :span="8">
-          <div class="info-item">
-            <span class="label">创建时间:</span>
-            <span class="value">{{ formatDate(firmwareInfo.createTime) }} </span>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-red-500">{{ statistics.failed || 0 }}</div>
+            <div class="text-14px text-gray-600">升级失败</div>
           </div>
         </el-col>
-      </el-row>
-      <el-row :gutter="20" class="mt-10px">
-        <el-col :span="24">
-          <div class="info-item">
-            <span class="label">固件描述:</span>
-            <span class="value">{{ firmwareInfo.description }}</span>
-          </div>
-        </el-col>
-      </el-row>
-    </ContentWrap>
-
-    <!-- 固件升级设备统计 -->
-    <!-- TODO @AI:字段不太对;还有待推送、已推送、升级取消 -->
-    <ContentWrap title="固件升级设备统计" class="mb-20px">
-      <el-row :gutter="20" class="statistics-row" v-loading="statisticsLoading">
-        <el-col :span="6">
-          <div class="stat-card">
-            <div class="stat-number text-primary">{{ statistics.total || 0 }}</div>
-            <div class="stat-label">升级设备总数</div>
-          </div>
-        </el-col>
-        <el-col :span="6">
-          <div class="stat-card">
-            <div class="stat-number text-success">{{ statistics.success || 0 }}</div>
-            <div class="stat-label">升级成功</div>
-          </div>
-        </el-col>
-        <el-col :span="6">
-          <div class="stat-card">
-            <div class="stat-number text-warning">{{ statistics.inProgress || 0 }}</div>
-            <div class="stat-label">正在升级</div>
-          </div>
-        </el-col>
-        <el-col :span="6">
-          <div class="stat-card">
-            <div class="stat-number text-danger">{{ statistics.failed || 0 }}</div>
-            <div class="stat-label">升级失败</div>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-gray-400">
+              {{ statistics.cancelled || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级取消</div>
           </div>
         </el-col>
       </el-row>
     </ContentWrap>
 
     <!-- 任务管理 -->
-    <ContentWrap title="任务管理" class="mb-20px">
-      <!-- 搜索栏 -->
-      <el-form
-        class="-mb-15px"
-        :model="queryParams"
-        ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-      >
-        <el-form-item>
-          <el-button type="primary" @click="openTaskForm">
-            <Icon icon="ep:plus" class="mr-5px" /> 新增
-          </el-button>
-        </el-form-item>
-        <el-form-item style="float: right">
-          <el-input
-            v-model="queryParams.name"
-            placeholder="请输入任务名称"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-240px"
-          />
-        </el-form-item>
-      </el-form>
-
-      <!-- 任务列表 -->
-      <el-table
-        v-loading="taskLoading"
-        :data="taskList"
-        :stripe="true"
-        :show-overflow-tooltip="true"
-        class="mt-15px"
-      >
-        <el-table-column label="任务编号" align="center" prop="id" width="80" />
-        <el-table-column label="任务名称" align="center" prop="name" />
-        <!-- TODO @AI:字典 iot_ota_task_device_scope -->
-        <el-table-column label="升级范围" align="center" prop="deviceScope">
-          <template #default="scope">
-            <el-tag type="warning">{{ getDeviceScopeText(scope.row.deviceScope) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="升级进度" align="center">
-          <template #default="scope">
-            {{ scope.row.deviceSuccessCount }}/{{ scope.row.deviceTotalCount }}
-          </template>
-        </el-table-column>
-        <el-table-column
-          label="创建时间"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-        />
-        <el-table-column label="任务描述" align="center" prop="description" show-overflow-tooltip />
-        <!-- TODO @AI:字典 iot_ota_task_status -->
-        <el-table-column label="任务状态" align="center" prop="status">
-          <template #default="scope">
-            <el-tag :type="getStatusTagType(scope.row.status)">
-              {{ getStatusText(scope.row.status) }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" width="120">
-          <template #default="scope">
-            <!-- TODO @AI:枚举下字段; -->
-            <el-button
-              v-if="scope.row.status === 1"
-              link
-              type="primary"
-              @click="handleCancelTask(scope.row.id)"
-              v-hasPermi="['iot:ota-task:cancel']"
-            >
-              取消
-            </el-button>
-            <!-- TODO @AI:不支持删除 -->
-            <el-button
-              link
-              type="danger"
-              @click="handleDeleteTask(scope.row.id)"
-              v-hasPermi="['iot:ota-task:delete']"
-            >
-              删除
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <!-- 分页 -->
-      <Pagination
-        :total="taskTotal"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getTaskList"
-      />
-    </ContentWrap>
-
-    <!-- 新增任务弹窗 -->
-    <!-- TODO @AI:搞成独立组件,放到 task 目录 -->
-    <el-dialog v-model="taskFormVisible" title="新增升级任务" width="600px" append-to-body>
-      <el-form ref="taskFormRef" :model="taskForm" :rules="taskFormRules" label-width="100px">
-        <el-form-item label="任务名称" prop="taskName">
-          <el-input v-model="taskForm.taskName" placeholder="请输入任务名称" />
-        </el-form-item>
-        <el-form-item label="任务类型" prop="taskType">
-          <el-select v-model="taskForm.taskType" placeholder="请选择任务类型" class="w-full">
-            <el-option label="整体升级" value="全量升级" />
-            <el-option label="批量升级" value="批量升级" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="预定时间" prop="scheduledTime">
-          <el-date-picker
-            v-model="taskForm.scheduledTime"
-            type="datetime"
-            placeholder="请选择预定时间"
-            format="YYYY-MM-DD HH:mm:ss"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            class="w-full"
-          />
-        </el-form-item>
-        <el-form-item label="任务描述" prop="description">
-          <el-input
-            v-model="taskForm.description"
-            type="textarea"
-            :rows="3"
-            placeholder="请输入任务描述"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="taskFormVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmitTask" :loading="taskSubmitting">
-          确定
-        </el-button>
-      </template>
-    </el-dialog>
+    <OtaTaskList ref="otaTaskListRef" :firmware-id="firmwareId" />
   </div>
 </template>
 
 <script setup lang="ts">
-import { dateFormatter, formatDate } from '@/utils/formatTime'
+import { formatDate } from '@/utils/formatTime'
 import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
-import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
 import { IoTOtaTaskRecordApi } from '@/api/iot/ota/task/record'
-import { ProductApi } from '@/api/iot/product/product'
+import { IoTOtaTaskRecordStatusEnum } from '@/views/iot/utils/constants'
+import OtaTaskList from '../../task/OtaTaskList.vue'
 
 /** IoT OTA 固件详情 */
 defineOptions({ name: 'IoTOtaFirmwareDetail' })
 
-const message = useMessage() // 消息弹窗
 const route = useRoute()
 const firmwareId = ref(Number(route.params.id))
 
 // 固件信息
 const firmwareLoading = ref(false)
-const firmwareInfo = ref<IoTOtaFirmware>({
-  id: 0,
-  name: '',
-  description: '',
-  version: '',
-  productId: 0,
-  createTime: ''
-})
-const productName = ref('')
-const isLatest = ref(true)
+const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware)
 
 // 统计信息
 const statisticsLoading = ref(false)
 const statistics = ref({
   total: 0,
-  success: 0,
+  pending: 0,
+  pushed: 0,
   inProgress: 0,
-  failed: 0
-})
-
-// 任务列表
-const taskLoading = ref(false)
-const taskList = ref<OtaTask[]>([])
-const taskTotal = ref(0)
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  name: '',
-  firmwareId: firmwareId.value
+  success: 0,
+  failed: 0,
+  cancelled: 0
 })
-const queryFormRef = ref()
 
-// 任务表单
-const taskFormVisible = ref(false)
-const taskSubmitting = ref(false)
-const taskForm = ref<OtaTask>({
-  name: '',
-  deviceScope: undefined,
-  firmwareId: firmwareId.value,
-  description: ''
-})
-const taskFormRef = ref()
-const taskFormRules = {
-  name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
-  deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' }]
-}
+// 任务列表组件引用
+const otaTaskListRef = ref()
 
 /** 获取固件信息 */
 const getFirmwareInfo = async () => {
   firmwareLoading.value = true
   try {
-    const data = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value)
-    firmwareInfo.value = data
-    // 获取产品名称
-    if (data.productId) {
-      const product = await ProductApi.getProduct(data.productId)
-      productName.value = product.name
-    }
+    firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value)
   } finally {
     firmwareLoading.value = false
   }
@@ -309,173 +130,30 @@ const getStatistics = async () => {
   try {
     const data = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusCount(firmwareId.value)
     statistics.value = {
-      total: (data[1] || 0) + (data[2] || 0) + (data[3] || 0) + (data[4] || 0), // 假设状态:1待升级,2升级中,3成功,4失败
-      success: data[3] || 0,
-      inProgress: data[2] || 0,
-      failed: data[4] || 0
+      pending: data[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0,
+      pushed: data[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0,
+      inProgress: data[IoTOtaTaskRecordStatusEnum.IN_PROGRESS.value] || 0,
+      success: data[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0,
+      failed: data[IoTOtaTaskRecordStatusEnum.FAILED.value] || 0,
+      cancelled: data[IoTOtaTaskRecordStatusEnum.CANCELLED.value] || 0,
+      total: 0
     }
+    // 计算总数
+    statistics.value.total =
+      statistics.value.pending +
+      statistics.value.pushed +
+      statistics.value.inProgress +
+      statistics.value.success +
+      statistics.value.failed +
+      statistics.value.cancelled
   } finally {
     statisticsLoading.value = false
   }
 }
 
-/** 获取任务列表 */
-const getTaskList = async () => {
-  taskLoading.value = true
-  try {
-    const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams)
-    taskList.value = data.list
-    taskTotal.value = data.total
-  } finally {
-    taskLoading.value = false
-  }
-}
-
-/** 搜索 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getTaskList()
-}
-
-/** 获取任务类型文本 */
-const getTaskTypeText = (taskType: string) => {
-  return taskType || '全量升级'
-}
-
-/** 获取状态文本 */
-const getStatusText = (status: number) => {
-  const statusMap = {
-    1: '待执行',
-    2: '执行中',
-    3: '已完成',
-    4: '已取消',
-    5: '执行失败'
-  }
-  return statusMap[status] || '未知'
-}
-
-/** 获取状态标签类型 */
-const getStatusTagType = (status: number) => {
-  const typeMap = {
-    1: 'info',
-    2: 'warning',
-    3: 'success',
-    4: 'info',
-    5: 'danger'
-  }
-  return typeMap[status] || 'info'
-}
-
-/** 打开任务表单 */
-const openTaskForm = () => {
-  taskForm.value = {
-    taskName: '',
-    taskType: '',
-    firmwareId: firmwareId.value,
-    scheduledTime: '',
-    description: ''
-  }
-  taskFormVisible.value = true
-}
-
-/** 提交任务 */
-const handleSubmitTask = async () => {
-  try {
-    await taskFormRef.value.validate()
-    taskSubmitting.value = true
-    await IoTOtaTaskApi.createOtaTask(taskForm.value)
-    message.success('创建成功')
-    taskFormVisible.value = false
-    getTaskList()
-  } catch (error) {
-    console.error('创建任务失败', error)
-  } finally {
-    taskSubmitting.value = false
-  }
-}
-
-/** 取消任务 */
-const handleCancelTask = async (id: number) => {
-  try {
-    await message.confirm('确认要取消该升级任务吗?')
-    await IoTOtaTaskApi.cancelOtaTask(id)
-    message.success('取消成功')
-    getTaskList()
-  } catch (error) {
-    console.error('取消任务失败', error)
-  }
-}
-
-/** 删除任务 */
-const handleDeleteTask = async (id: number) => {
-  try {
-    await message.confirm('确认要删除该升级任务吗?')
-    // 这里应该调用删除接口,但提供的代码中没有删除接口
-    message.success('删除成功')
-    getTaskList()
-  } catch (error) {
-    console.error('删除任务失败', error)
-  }
-}
-
 /** 初始化 */
 onMounted(() => {
   getFirmwareInfo()
   getStatistics()
-  getTaskList()
 })
 </script>
-
-<style scoped>
-.info-item {
-  padding: 8px 0;
-}
-
-.info-item .label {
-  font-weight: 500;
-  color: #606266;
-}
-
-.info-item .value {
-  color: #303133;
-}
-
-.statistics-row {
-  padding: 20px 0;
-}
-
-.stat-card {
-  text-align: center;
-  padding: 20px;
-  border: 1px solid #ebeef5;
-  border-radius: 4px;
-  background-color: #fafafa;
-}
-
-.stat-number {
-  font-size: 32px;
-  font-weight: bold;
-  margin-bottom: 8px;
-}
-
-.stat-label {
-  font-size: 14px;
-  color: #606266;
-}
-
-.text-primary {
-  color: #409eff;
-}
-
-.text-success {
-  color: #67c23a;
-}
-
-.text-warning {
-  color: #e6a23c;
-}
-
-.text-danger {
-  color: #f56c6c;
-}
-</style>

+ 111 - 0
src/views/iot/ota/task/OtaTaskForm.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="新增升级任务" width="600px" append-to-body>
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+      <el-form-item label="任务名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入任务名称" />
+      </el-form-item>
+      <el-form-item label="升级范围" prop="deviceScope">
+        <el-select v-model="formData.deviceScope" placeholder="请选择升级范围" class="w-full">
+          <el-option
+            v-for="item in Object.values(IoTOtaTaskDeviceScopeEnum)"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="任务描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          :rows="3"
+          placeholder="请输入任务描述"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="handleCancel">取消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitting"> 确定 </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { IoTOtaTaskDeviceScopeEnum } from '@/views/iot/utils/constants'
+
+defineOptions({ name: 'OtaTaskForm' })
+
+const props = defineProps<{
+  firmwareId: number
+}>()
+
+const emit = defineEmits<{
+  success: []
+}>()
+
+const message = useMessage()
+
+// 弹窗状态
+const dialogVisible = ref(false)
+const submitting = ref(false)
+
+// 表单数据
+const formRef = ref()
+const formData = ref<OtaTask>({
+  name: '',
+  deviceScope: undefined,
+  firmwareId: props.firmwareId,
+  description: ''
+})
+
+// 表单验证规则
+const formRules = {
+  name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
+  deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' }]
+}
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+  resetForm()
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    name: '',
+    deviceScope: undefined,
+    firmwareId: props.firmwareId,
+    description: ''
+  }
+  nextTick(() => {
+    formRef.value?.clearValidate()
+  })
+}
+
+/** 取消 */
+const handleCancel = () => {
+  dialogVisible.value = false
+}
+
+/** 提交表单 */
+const handleSubmit = async () => {
+  try {
+    await formRef.value.validate()
+    submitting.value = true
+    await IoTOtaTaskApi.createOtaTask(formData.value)
+    message.success('创建成功')
+    dialogVisible.value = false
+    emit('success')
+  } catch (error) {
+    console.error('创建任务失败', error)
+  } finally {
+    submitting.value = false
+  }
+}
+
+defineExpose({
+  open
+})
+</script>

+ 162 - 0
src/views/iot/ota/task/OtaTaskList.vue

@@ -0,0 +1,162 @@
+<template>
+  <ContentWrap title="任务管理" class="mb-20px">
+    <!-- 搜索栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item>
+        <el-button type="primary" @click="openTaskForm">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+      <el-form-item style="float: right">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入任务名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+
+    <!-- 任务列表 -->
+    <el-table
+      v-loading="taskLoading"
+      :data="taskList"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      class="mt-15px"
+    >
+      <el-table-column label="任务编号" align="center" prop="id" width="80" />
+      <el-table-column label="任务名称" align="center" prop="name" />
+      <el-table-column label="升级范围" align="center" prop="deviceScope">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE" :value="scope.row.deviceScope" />
+        </template>
+      </el-table-column>
+      <el-table-column label="升级进度" align="center">
+        <template #default="scope">
+          {{ scope.row.deviceSuccessCount }}/{{ scope.row.deviceTotalCount }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="任务描述" align="center" prop="description" show-overflow-tooltip />
+      <el-table-column label="任务状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="80">
+        <template #default="scope">
+          <el-button
+            v-if="scope.row.status === IoTOtaTaskStatusEnum.PENDING.value"
+            link
+            type="primary"
+            @click="handleCancelTask(scope.row.id)"
+            v-hasPermi="['iot:ota-task:cancel']"
+          >
+            取消
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="taskTotal"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getTaskList"
+    />
+
+    <!-- 新增任务弹窗 -->
+    <OtaTaskForm ref="taskFormRef" :firmware-id="firmwareId" @success="getTaskList" />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { DICT_TYPE } from '@/utils/dict'
+import { IoTOtaTaskStatusEnum } from '@/views/iot/utils/constants'
+import OtaTaskForm from './OtaTaskForm.vue'
+
+/** IoT OTA 任务列表 */
+defineOptions({ name: 'OtaTaskList' })
+
+const props = defineProps<{
+  firmwareId: number
+}>()
+
+const message = useMessage() // 消息弹窗
+
+// 任务列表
+const taskLoading = ref(false)
+const taskList = ref<OtaTask[]>([])
+const taskTotal = ref(0)
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  firmwareId: props.firmwareId
+})
+const queryFormRef = ref()
+
+// 任务表单引用
+const taskFormRef = ref()
+
+/** 获取任务列表 */
+const getTaskList = async () => {
+  taskLoading.value = true
+  try {
+    const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams)
+    taskList.value = data.list
+    taskTotal.value = data.total
+  } finally {
+    taskLoading.value = false
+  }
+}
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getTaskList()
+}
+
+/** 打开任务表单 */
+const openTaskForm = () => {
+  taskFormRef.value?.open()
+}
+
+/** 取消任务 */
+const handleCancelTask = async (id: number) => {
+  try {
+    await message.confirm('确认要取消该升级任务吗?')
+    await IoTOtaTaskApi.cancelOtaTask(id)
+    message.success('取消成功')
+    getTaskList()
+  } catch (error) {
+    console.error('取消任务失败', error)
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getTaskList()
+})
+
+/** 暴露方法供父组件调用 */
+defineExpose({
+  getTaskList
+})
+</script>

+ 64 - 0
src/views/iot/utils/constants.ts

@@ -147,3 +147,67 @@ export const getDataTypeOptionsLabel = (value: string) => {
   const dataType = getDataTypeOptions().find((option) => option.value === value)
   return dataType && `${dataType.value}(${dataType.label})`
 }
+
+// IoT OTA 任务设备范围枚举
+export const IoTOtaTaskDeviceScopeEnum = {
+  ALL: {
+    label: '全部设备',
+    value: 1
+  },
+  SPECIFIC: {
+    label: '指定设备',
+    value: 2
+  }
+} as const
+
+// IoT OTA 任务状态枚举
+export const IoTOtaTaskStatusEnum = {
+  PENDING: {
+    label: '待执行',
+    value: 1
+  },
+  IN_PROGRESS: {
+    label: '执行中',
+    value: 2
+  },
+  COMPLETED: {
+    label: '已完成',
+    value: 3
+  },
+  CANCELLED: {
+    label: '已取消',
+    value: 4
+  },
+  FAILED: {
+    label: '执行失败',
+    value: 5
+  }
+} as const
+
+// IoT OTA 升级记录状态枚举
+export const IoTOtaTaskRecordStatusEnum = {
+  PENDING: {
+    label: '待推送',
+    value: 1
+  },
+  PUSHED: {
+    label: '已推送',
+    value: 2
+  },
+  IN_PROGRESS: {
+    label: '正在升级',
+    value: 3
+  },
+  SUCCESS: {
+    label: '升级成功',
+    value: 4
+  },
+  FAILED: {
+    label: '升级失败',
+    value: 5
+  },
+  CANCELLED: {
+    label: '升级取消',
+    value: 6
+  }
+} as const