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

pull/789/MERGE
YunaiV 2025-07-02 23:30:09 +08:00
parent 667d6fc35c
commit a5cb8e510c
5 changed files with 113 additions and 178 deletions

View File

@ -3,15 +3,15 @@ import request from '@/config/axios'
/** IoT OTA 任务信息 */ /** IoT OTA 任务信息 */
export interface OtaTask { export interface OtaTask {
id?: number // 任务编号 id?: number // 任务编号
name?: string // 任务名称 name: string // 任务名称
description?: string // 任务描述 description?: string // 任务描述
firmwareId?: number // 固件编号 firmwareId?: number // 固件编号
status?: number // 任务状态 status: number // 任务状态
deviceScope?: number // 升级范围 deviceScope?: number // 升级范围
deviceIds?: number[] // 指定设备ID列表当升级范围为指定设备时使用 deviceIds?: number[] // 指定设备ID列表当升级范围为指定设备时使用
deviceTotalCount?: number // 设备总共数量 deviceTotalCount?: number // 设备总共数量
deviceSuccessCount?: number // 设备成功数量 deviceSuccessCount?: number // 设备成功数量
createTime?: string // 创建时间 createTime?: Date // 创建时间
} }
// IoT OTA 任务 API // IoT OTA 任务 API

View File

@ -7,11 +7,14 @@ export interface OtaTaskRecord {
firmwareVersion?: string // 固件版本 firmwareVersion?: string // 固件版本
taskId?: number // 任务编号 taskId?: number // 任务编号
deviceId?: string // 设备编号 deviceId?: string // 设备编号
deviceName?: string // 设备名称
currentVersion?: string // 当前版本
fromFirmwareId?: number // 来源的固件编号 fromFirmwareId?: number // 来源的固件编号
fromFirmwareVersion?: string // 来源的固件版本 fromFirmwareVersion?: string // 来源的固件版本
status?: number // 升级状态 status?: number // 升级状态
progress?: number // 升级进度,百分比 progress?: number // 升级进度,百分比
description?: string // 升级进度描述 description?: string // 升级进度描述
updateTime?: Date // 更新时间
} }
// IoT OTA 任务记录 API // IoT OTA 任务记录 API
@ -31,5 +34,10 @@ export const IoTOtaTaskRecordApi = {
// 查询 OTA 任务记录详情 // 查询 OTA 任务记录详情
getOtaTaskRecord: async (id: number) => { getOtaTaskRecord: async (id: number) => {
return await request.get({ url: `/iot/ota/task/record/get?id=` + id }) return await request.get({ url: `/iot/ota/task/record/get?id=` + id })
},
// 取消 OTA 任务记录
cancelOtaTaskRecord: async (id: number) => {
return await request.post({ url: `/iot/ota/task/record/cancel?id=` + id })
} }
} }

View File

@ -244,5 +244,5 @@ export enum DICT_TYPE {
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_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA任务设备范围
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态 IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
IOT_OTA_RECORD_STATUS = 'iot_ota_record_status' // IoT OTA 记录状态 IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status' // IoT OTA 记录状态
} }

View File

@ -104,16 +104,14 @@ import OtaTaskList from '../../task/OtaTaskList.vue'
/** IoT OTA 固件详情 */ /** IoT OTA 固件详情 */
defineOptions({ name: 'IoTOtaFirmwareDetail' }) defineOptions({ name: 'IoTOtaFirmwareDetail' })
const route = useRoute() const route = useRoute() //
const firmwareId = ref(Number(route.params.id))
// const firmwareId = ref(Number(route.params.id)) //
const firmwareLoading = ref(false) const firmwareLoading = ref(false) //
const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware) const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware) //
// const firmwareStatisticsLoading = ref(false) //
const firmwareStatisticsLoading = ref(false) const firmwareStatistics = ref<Record<string, number>>({}) //
const firmwareStatistics = ref<Record<string, number>>({})
/** 获取固件信息 */ /** 获取固件信息 */
const getFirmwareInfo = async () => { const getFirmwareInfo = async () => {

View File

@ -3,71 +3,80 @@
<!-- 任务信息 --> <!-- 任务信息 -->
<ContentWrap title="任务信息" class="mb-20px"> <ContentWrap title="任务信息" class="mb-20px">
<el-descriptions :column="3" v-loading="taskLoading"> <el-descriptions :column="3" v-loading="taskLoading">
<el-descriptions-item label="任务ID">{{ taskInfo.id }}</el-descriptions-item> <el-descriptions-item label="任务编号">{{ task.id }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ taskInfo.name }}</el-descriptions-item> <el-descriptions-item label="任务名称">{{ task.name }}</el-descriptions-item>
<el-descriptions-item label="任务类型">版本升级</el-descriptions-item> <el-descriptions-item label="升级范围">
<el-descriptions-item label="设备数量">{{ <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE" :value="task.deviceScope" />
taskInfo.deviceTotalCount </el-descriptions-item>
}}</el-descriptions-item> <el-descriptions-item label="任务状态">
<el-descriptions-item label="预定时间">-</el-descriptions-item> <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_STATUS" :value="task.status" />
<el-descriptions-item label="添加时间">{{ </el-descriptions-item>
formatTime(taskInfo.createTime) <el-descriptions-item label="创建时间">
}}</el-descriptions-item> {{ task.createTime ? formatDate(task.createTime) : '-' }}
<el-descriptions-item label="任务描述" :span="3">{{ </el-descriptions-item>
taskInfo.description || '-' <el-descriptions-item label="任务描述" :span="3">
}}</el-descriptions-item> {{ task.description }}
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</ContentWrap> </ContentWrap>
<!-- 任务升级设备统计 --> <!-- 任务升级设备统计 -->
<ContentWrap title="升级设备统计" class="mb-20px"> <ContentWrap title="升级设备统计" class="mb-20px">
<el-row :gutter="20" class="py-20px" v-loading="statisticsLoading"> <el-row :gutter="20" class="py-20px" v-loading="taskStatisticsLoading">
<el-col :span="4"> <el-col :span="6">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50"> <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"> <div class="text-32px font-bold mb-8px text-blue-500">
{{ statisticsData.total }} {{ Object.values(taskStatistics).reduce((sum, count) => sum + (count || 0), 0) || 0 }}
</div> </div>
<div class="text-14px text-gray-600">升级设备总数</div> <div class="text-14px text-gray-600">升级设备总数</div>
</div> </div>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="3">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50"> <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"> <div class="text-32px font-bold mb-8px text-gray-400">
{{ statisticsData.pending }} {{ taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
</div> </div>
<div class="text-14px text-gray-600">待推送</div> <div class="text-14px text-gray-600">待推送</div>
</div> </div>
</el-col> </el-col>
<el-col :span="4"> <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">
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
</div>
<div class="text-14px text-gray-600">已推送</div>
</div>
</el-col>
<el-col :span="3">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50"> <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"> <div class="text-32px font-bold mb-8px text-yellow-500">
{{ statisticsData.upgrading }} {{ taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
</div> </div>
<div class="text-14px text-gray-600">正在升级</div> <div class="text-14px text-gray-600">正在升级</div>
</div> </div>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="3">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50"> <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"> <div class="text-32px font-bold mb-8px text-green-500">
{{ statisticsData.success }} {{ taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
</div> </div>
<div class="text-14px text-gray-600">升级成功</div> <div class="text-14px text-gray-600">升级成功</div>
</div> </div>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="3">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50"> <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"> <div class="text-32px font-bold mb-8px text-red-500">
{{ statisticsData.failure }} {{ taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
</div> </div>
<div class="text-14px text-gray-600">升级失败</div> <div class="text-14px text-gray-600">升级失败</div>
</div> </div>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="3">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50"> <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"> <div class="text-32px font-bold mb-8px text-gray-400">
{{ statisticsData.stopped }} {{ taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
</div> </div>
<div class="text-14px text-gray-600">停止</div> <div class="text-14px text-gray-600">升级取消</div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
@ -89,10 +98,10 @@
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
> >
<el-table-column label="设备名称" align="center" prop="deviceName" /> <el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="当前版本" align="center" prop="currentVersion" /> <el-table-column label="当前版本" align="center" prop="fromFirmwareVersion" />
<el-table-column label="升级状态" align="center" prop="status" width="120"> <el-table-column label="升级状态" align="center" prop="status" width="120">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_OTA_RECORD_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_RECORD_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="升级进度" align="center" prop="progress" width="120"> <el-table-column label="升级进度" align="center" prop="progress" width="120">
@ -100,7 +109,9 @@
</el-table-column> </el-table-column>
<el-table-column label="状态描述" align="center" prop="description" /> <el-table-column label="状态描述" align="center" prop="description" />
<el-table-column label="更新时间" align="center" prop="updateTime" width="180"> <el-table-column label="更新时间" align="center" prop="updateTime" width="180">
<template #default="scope"> {{ formatTime(scope.row.updateTime) }} </template> <template #default="scope">
{{ formatDate(scope.row.updateTime) }}
</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" width="80"> <el-table-column label="操作" align="center" width="80">
<template #default="scope"> <template #default="scope">
@ -116,7 +127,6 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<div class="flex justify-center mt-20px">
<Pagination <Pagination
:total="recordTotal" :total="recordTotal"
v-model:page="queryParams.pageNo" v-model:page="queryParams.pageNo"
@ -124,7 +134,6 @@
@pagination="getRecordList" @pagination="getRecordList"
/> />
</div> </div>
</div>
</ContentWrap> </ContentWrap>
</Dialog> </Dialog>
</template> </template>
@ -144,90 +153,48 @@ import { formatDate } from '@/utils/formatTime'
/** OTA 任务详情组件 */ /** OTA 任务详情组件 */
defineOptions({ name: 'OtaTaskDetail' }) defineOptions({ name: 'OtaTaskDetail' })
const message = useMessage() const message = useMessage() //
// const dialogVisible = ref(false) //
const dialogVisible = ref(false)
const taskId = ref<number>()
// const taskId = ref<number>() //
const taskLoading = ref(false) const taskLoading = ref(false) //
const taskInfo = ref<OtaTask>({}) const task = ref<OtaTask>({} as OtaTask) //
// const taskStatisticsLoading = ref(false) //
const statisticsLoading = ref(false) const taskStatistics = ref<Record<string, number>>({}) //
// const recordLoading = ref(false) //
const statisticsData = ref({ const recordList = ref<OtaTaskRecord[]>([]) //
total: 0, const recordTotal = ref(0) //
pending: 0,
pushed: 0,
upgrading: 0,
success: 0,
failure: 0,
stopped: 0
})
//
const activeTab = ref('')
//
const recordLoading = ref(false)
const recordList = ref<OtaTaskRecord[]>([])
const recordTotal = ref(0)
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
taskId: undefined as number | undefined, taskId: undefined as number | undefined,
status: undefined as number | undefined, status: undefined as number | undefined
deviceNumber: '' }) //
const activeTab = ref('') //
/** 状态标签配置 */
const statusTabs = computed(() => {
const tabs = [{ key: '', label: '全部设备' }]
Object.values(IoTOtaTaskRecordStatusEnum).forEach((status) => {
tabs.push({
key: status.value.toString(),
label: status.label
})
})
return tabs
}) })
//
const statusTabs = computed(() => [
{ key: '', label: '全部设备' },
{
key: IoTOtaTaskRecordStatusEnum.PENDING.value.toString(),
label: IoTOtaTaskRecordStatusEnum.PENDING.label
},
{
key: IoTOtaTaskRecordStatusEnum.PUSHED.value.toString(),
label: IoTOtaTaskRecordStatusEnum.PUSHED.label
},
{
key: IoTOtaTaskRecordStatusEnum.UPGRADING.value.toString(),
label: IoTOtaTaskRecordStatusEnum.UPGRADING.label
},
{
key: IoTOtaTaskRecordStatusEnum.SUCCESS.value.toString(),
label: IoTOtaTaskRecordStatusEnum.SUCCESS.label
},
{
key: IoTOtaTaskRecordStatusEnum.FAILURE.value.toString(),
label: IoTOtaTaskRecordStatusEnum.FAILURE.label
},
{
key: IoTOtaTaskRecordStatusEnum.CANCELED.value.toString(),
label: IoTOtaTaskRecordStatusEnum.CANCELED.label
}
])
/** 时间格式化 */
const formatTime = (time: string | undefined) => {
if (!time) return '-'
return formatDate(new Date(time))
}
/** 获取任务详情 */ /** 获取任务详情 */
const getTaskInfo = async () => { const getTaskInfo = async () => {
if (!taskId.value) return if (!taskId.value) {
return
}
taskLoading.value = true taskLoading.value = true
try { try {
const data = await IoTOtaTaskApi.getOtaTask(taskId.value) task.value = await IoTOtaTaskApi.getOtaTask(taskId.value)
taskInfo.value = data
} catch (error) {
console.error('获取任务详情失败', error)
} finally { } finally {
taskLoading.value = false taskLoading.value = false
} }
@ -235,61 +202,31 @@ const getTaskInfo = async () => {
/** 获取统计数据 */ /** 获取统计数据 */
const getStatistics = async () => { const getStatistics = async () => {
if (!taskId.value) return if (!taskId.value) {
return
statisticsLoading.value = true }
taskStatisticsLoading.value = true
try { try {
const data = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(undefined, taskId.value) taskStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
statisticsData.value = { undefined,
total: data.total || 0, taskId.value
pending: data.pending || 0, )
pushed: data.pushed || 0,
upgrading: data.upgrading || 0,
success: data.success || 0,
failure: data.failure || 0,
stopped: data.stopped || 0
}
} catch (error) {
console.error('获取统计数据失败', error)
//
statisticsData.value = {
total: 1,
pending: 0,
pushed: 0,
upgrading: 0,
success: 0,
failure: 1,
stopped: 0
}
} finally { } finally {
statisticsLoading.value = false taskStatisticsLoading.value = false
} }
} }
/** 获取记录列表 */ /** 获取升级记录列表 */
const getRecordList = async () => { const getRecordList = async () => {
if (!taskId.value) return if (!taskId.value) {
return
}
recordLoading.value = true recordLoading.value = true
try { try {
queryParams.taskId = taskId.value queryParams.taskId = taskId.value
const data = await IoTOtaTaskRecordApi.getOtaTaskRecordPage(queryParams) const data = await IoTOtaTaskRecordApi.getOtaTaskRecordPage(queryParams)
recordList.value = data.list || [] recordList.value = data.list || []
recordTotal.value = data.total || 0 recordTotal.value = data.total || 0
} catch (error) {
console.error('获取记录列表失败', error)
//
recordList.value = [
{
id: 1,
taskId: taskId.value,
deviceId: '1',
status: IoTOtaTaskRecordStatusEnum.FAILURE.value,
progress: 0,
description: '升级失败'
} as OtaTaskRecord
]
recordTotal.value = 1
} finally { } finally {
recordLoading.value = false recordLoading.value = false
} }
@ -300,14 +237,7 @@ const handleTabClick = (tab: TabsPaneContext) => {
const tabKey = tab.paneName as string const tabKey = tab.paneName as string
activeTab.value = tabKey activeTab.value = tabKey
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.status = activeTab.value === '' ? undefined : parseInt(tabKey)
// 使 IoTOtaTaskRecordStatusEnum tab key
if (tabKey === '') {
queryParams.status = undefined //
} else {
queryParams.status = parseInt(tabKey) // 使
}
getRecordList() getRecordList()
} }
@ -315,9 +245,12 @@ const handleTabClick = (tab: TabsPaneContext) => {
const handleCancelUpgrade = async (record: OtaTaskRecord) => { const handleCancelUpgrade = async (record: OtaTaskRecord) => {
try { try {
await message.confirm('确认要取消该设备的升级任务吗?') await message.confirm('确认要取消该设备的升级任务吗?')
// TODO: await IoTOtaTaskRecordApi.cancelOtaTaskRecord(record.id!)
message.success('取消成功') message.success('取消成功')
getRecordList() //
await getRecordList()
await getStatistics()
// TODO @AI succes
} catch (error) { } catch (error) {
console.error('取消升级失败', error) console.error('取消升级失败', error)
} }
@ -327,12 +260,10 @@ const handleCancelUpgrade = async (record: OtaTaskRecord) => {
const open = (id: number) => { const open = (id: number) => {
taskId.value = id taskId.value = id
dialogVisible.value = true dialogVisible.value = true
// //
activeTab.value = '' activeTab.value = ''
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.status = undefined queryParams.status = undefined
queryParams.deviceNumber = ''
// //
getTaskInfo() getTaskInfo()
@ -341,7 +272,5 @@ const open = (id: number) => {
} }
/** 暴露方法 */ /** 暴露方法 */
defineExpose({ defineExpose({ open })
open
})
</script> </script>