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

pull/789/MERGE
YunaiV 2025-07-02 19:02:41 +08:00
parent 5be7dde543
commit 667d6fc35c
8 changed files with 480 additions and 72 deletions

View File

@ -118,8 +118,13 @@ export const DeviceApi = {
},
// 获取设备的精简信息列表
getSimpleDeviceList: async (deviceType?: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } })
getSimpleDeviceList: async (deviceType?: number, productId?: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType, productId } })
},
// 根据产品编号,获取设备的精简信息列表
getDeviceListByProductId: async (productId: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { productId } })
},
// 获取导入模板

View File

@ -8,6 +8,7 @@ export interface OtaTask {
firmwareId?: number // 固件编号
status?: number // 任务状态
deviceScope?: number // 升级范围
deviceIds?: number[] // 指定设备ID列表当升级范围为指定设备时使用
deviceTotalCount?: number // 设备总共数量
deviceSuccessCount?: number // 设备成功数量
createTime?: string // 创建时间

View File

@ -243,5 +243,6 @@ export enum DICT_TYPE {
IOT_ALERT_LEVEL = 'iot_alert_level', // 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任务状态
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
IOT_OTA_RECORD_STATUS = 'iot_ota_record_status' // IoT OTA 记录状态
}

View File

@ -21,8 +21,8 @@
</el-descriptions>
</ContentWrap>
<!-- 固件升级设备统计 -->
<ContentWrap title="固件升级设备统计" class="mb-20px">
<!-- 升级设备统计 -->
<ContentWrap title="升级设备统计" class="mb-20px">
<el-row :gutter="20" class="py-20px" v-loading="firmwareStatisticsLoading">
<el-col :span="6">
<div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
@ -86,7 +86,11 @@
</ContentWrap>
<!-- 任务管理 -->
<OtaTaskList :firmware-id="firmwareId" />
<OtaTaskList
:firmware-id="firmwareId"
:product-id="firmware?.productId"
@success="getStatistics"
/>
</div>
</template>

View File

@ -0,0 +1,347 @@
<template>
<Dialog v-model="dialogVisible" title="升级任务详情" width="1200px" append-to-body>
<!-- 任务信息 -->
<ContentWrap title="任务信息" class="mb-20px">
<el-descriptions :column="3" v-loading="taskLoading">
<el-descriptions-item label="任务ID">{{ taskInfo.id }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ taskInfo.name }}</el-descriptions-item>
<el-descriptions-item label="任务类型">版本升级</el-descriptions-item>
<el-descriptions-item label="设备数量">{{
taskInfo.deviceTotalCount
}}</el-descriptions-item>
<el-descriptions-item label="预定时间">-</el-descriptions-item>
<el-descriptions-item label="添加时间">{{
formatTime(taskInfo.createTime)
}}</el-descriptions-item>
<el-descriptions-item label="任务描述" :span="3">{{
taskInfo.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="4">
<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">
{{ statisticsData.total }}
</div>
<div class="text-14px text-gray-600">升级设备总数</div>
</div>
</el-col>
<el-col :span="4">
<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">
{{ statisticsData.pending }}
</div>
<div class="text-14px text-gray-600">待推送</div>
</div>
</el-col>
<el-col :span="4">
<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">
{{ statisticsData.upgrading }}
</div>
<div class="text-14px text-gray-600">正在升级</div>
</div>
</el-col>
<el-col :span="4">
<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">
{{ statisticsData.success }}
</div>
<div class="text-14px text-gray-600">升级成功</div>
</div>
</el-col>
<el-col :span="4">
<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">
{{ statisticsData.failure }}
</div>
<div class="text-14px text-gray-600">升级失败</div>
</div>
</el-col>
<el-col :span="4">
<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">
{{ statisticsData.stopped }}
</div>
<div class="text-14px text-gray-600">停止</div>
</div>
</el-col>
</el-row>
</ContentWrap>
<!-- 设备管理 -->
<ContentWrap title="升级设备记录">
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-15px">
<el-tab-pane v-for="tab in statusTabs" :key="tab.key" :label="tab.label" :name="tab.key" />
</el-tabs>
<!-- Tab 内容 -->
<div v-for="tab in statusTabs" :key="tab.key" v-show="activeTab === tab.key">
<!-- 设备列表 -->
<el-table
v-loading="recordLoading"
:data="recordList"
:stripe="true"
:show-overflow-tooltip="true"
>
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="当前版本" align="center" prop="currentVersion" />
<el-table-column label="升级状态" align="center" prop="status" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_OTA_RECORD_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="升级进度" align="center" prop="progress" width="120">
<template #default="scope"> {{ scope.row.progress }}% </template>
</el-table-column>
<el-table-column label="状态描述" align="center" prop="description" />
<el-table-column label="更新时间" align="center" prop="updateTime" width="180">
<template #default="scope"> {{ formatTime(scope.row.updateTime) }} </template>
</el-table-column>
<el-table-column label="操作" align="center" width="80">
<template #default="scope">
<el-button
v-if="scope.row.status === IoTOtaTaskRecordStatusEnum.UPGRADING.value"
link
type="danger"
@click="handleCancelUpgrade(scope.row)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="flex justify-center mt-20px">
<Pagination
:total="recordTotal"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getRecordList"
/>
</div>
</div>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { TabsPaneContext } from 'element-plus'
import { Dialog } from '@/components/Dialog'
import { ContentWrap } from '@/components/ContentWrap'
import Pagination from '@/components/Pagination/index.vue'
import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
import { IoTOtaTaskRecordApi, OtaTaskRecord } from '@/api/iot/ota/task/record'
import { DICT_TYPE } from '@/utils/dict'
import { IoTOtaTaskRecordStatusEnum } from '@/views/iot/utils/constants'
import { formatDate } from '@/utils/formatTime'
/** OTA 任务详情组件 */
defineOptions({ name: 'OtaTaskDetail' })
const message = useMessage()
//
const dialogVisible = ref(false)
const taskId = ref<number>()
//
const taskLoading = ref(false)
const taskInfo = ref<OtaTask>({})
//
const statisticsLoading = ref(false)
//
const statisticsData = ref({
total: 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({
pageNo: 1,
pageSize: 10,
taskId: undefined as number | undefined,
status: undefined as number | undefined,
deviceNumber: ''
})
//
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 () => {
if (!taskId.value) return
taskLoading.value = true
try {
const data = await IoTOtaTaskApi.getOtaTask(taskId.value)
taskInfo.value = data
} catch (error) {
console.error('获取任务详情失败', error)
} finally {
taskLoading.value = false
}
}
/** 获取统计数据 */
const getStatistics = async () => {
if (!taskId.value) return
statisticsLoading.value = true
try {
const data = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(undefined, taskId.value)
statisticsData.value = {
total: data.total || 0,
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 {
statisticsLoading.value = false
}
}
/** 获取记录列表 */
const getRecordList = async () => {
if (!taskId.value) return
recordLoading.value = true
try {
queryParams.taskId = taskId.value
const data = await IoTOtaTaskRecordApi.getOtaTaskRecordPage(queryParams)
recordList.value = data.list || []
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 {
recordLoading.value = false
}
}
/** 切换标签 */
const handleTabClick = (tab: TabsPaneContext) => {
const tabKey = tab.paneName as string
activeTab.value = tabKey
queryParams.pageNo = 1
// 使 IoTOtaTaskRecordStatusEnum tab key
if (tabKey === '') {
queryParams.status = undefined //
} else {
queryParams.status = parseInt(tabKey) // 使
}
getRecordList()
}
/** 取消升级 */
const handleCancelUpgrade = async (record: OtaTaskRecord) => {
try {
await message.confirm('确认要取消该设备的升级任务吗?')
// TODO:
message.success('取消成功')
getRecordList()
} catch (error) {
console.error('取消升级失败', error)
}
}
/** 打开弹窗 */
const open = (id: number) => {
taskId.value = id
dialogVisible.value = true
//
activeTab.value = ''
queryParams.pageNo = 1
queryParams.status = undefined
queryParams.deviceNumber = ''
//
getTaskInfo()
getStatistics()
getRecordList()
}
/** 暴露方法 */
defineExpose({
open
})
</script>

View File

@ -1,9 +1,23 @@
<template>
<el-dialog v-model="dialogVisible" title="新增升级任务" width="600px" append-to-body>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-dialog v-model="dialogVisible" title="新增升级任务" width="800px" append-to-body>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="任务名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入任务名称" />
</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-item label="升级范围" prop="deviceScope">
<el-select v-model="formData.deviceScope" placeholder="请选择升级范围" class="w-full">
<el-option
@ -14,18 +28,33 @@
/>
</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
label="选择设备"
prop="deviceIds"
v-if="formData.deviceScope === IoTOtaTaskDeviceScopeEnum.SELECT.value"
>
<el-select
v-model="formData.deviceIds"
multiple
placeholder="请选择设备"
class="w-full"
filterable
reserve-keyword
>
<el-option
v-for="device in devices"
:key="device.id"
:label="
device.nickname ? `${device.deviceName} (${device.nickname})` : device.deviceName
"
:value="device.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel"></el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting"> 确定 </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</el-dialog>
</template>
@ -33,79 +62,71 @@
<script setup lang="ts">
import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
import { IoTOtaTaskDeviceScopeEnum } from '@/views/iot/utils/constants'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
/** IoT OTA 升级任务表单 */
defineOptions({ name: 'OtaTaskForm' })
const props = defineProps<{
firmwareId: number
productId: number
}>()
const emit = defineEmits<{
success: []
}>()
const message = useMessage() //
const message = useMessage()
//
const dialogVisible = ref(false)
const submitting = ref(false)
//
const formRef = ref()
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref<OtaTask>({
name: '',
deviceScope: undefined,
deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
firmwareId: props.firmwareId,
description: ''
description: '',
deviceIds: []
})
//
const formRef = ref() // Ref
const formRules = {
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' }]
deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' }],
deviceIds: [{ required: true, message: '请至少选择一个设备', trigger: 'change' }]
}
const devices = ref<DeviceVO[]>([]) //
/** 打开弹窗 */
const open = () => {
const open = async () => {
dialogVisible.value = true
resetForm()
//
devices.value = (await DeviceApi.getDeviceListByProductId(props.productId)) || []
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
await IoTOtaTaskApi.createOtaTask(formData.value)
message.success('创建成功')
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
name: '',
deviceScope: undefined,
deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
firmwareId: props.firmwareId,
description: ''
description: '',
deviceIds: []
}
nextTick(() => {
formRef.value?.clearValidate()
})
formRef.value?.resetFields()
}
/** 取消 */
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>

View File

@ -1,5 +1,5 @@
<template>
<ContentWrap title="固件任务管理" class="mb-20px">
<ContentWrap title="升级任务管理" class="mb-20px">
<!-- 搜索栏 -->
<el-form
class="-mb-15px"
@ -14,6 +14,7 @@
</el-button>
</el-form-item>
<el-form-item style="float: right">
<!--TODO @AI:有个 bug回车后会刷新修复下 -->
<el-input
v-model="queryParams.name"
placeholder="请输入任务名称"
@ -56,8 +57,10 @@
<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">
<el-table-column label="操作" align="center" width="120">
<!-- TODO @AI可能要参考别的模块处理下-->
<template #default="scope">
<el-button link type="primary" @click="handleViewDetail(scope.row.id)"> </el-button>
<el-button
v-if="scope.row.status === IoTOtaTaskStatusEnum.IN_PROGRESS.value"
link
@ -80,7 +83,15 @@
/>
<!-- 新增任务弹窗 -->
<OtaTaskForm ref="taskFormRef" :firmware-id="firmwareId" @success="getTaskList" />
<OtaTaskForm
ref="taskFormRef"
:firmware-id="firmwareId"
:product-id="productId"
@success="handleTaskSuccess"
/>
<!-- 任务详情弹窗 -->
<OtaTaskDetail ref="taskDetailRef" />
</ContentWrap>
</template>
@ -90,12 +101,18 @@ 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'
import OtaTaskDetail from './OtaTaskDetail.vue'
/** IoT OTA 任务列表 */
defineOptions({ name: 'OtaTaskList' })
const props = defineProps<{
firmwareId: number
productId?: number
}>()
const emit = defineEmits<{
success: []
}>()
const message = useMessage() //
@ -110,10 +127,9 @@ const queryParams = reactive({
name: '',
firmwareId: props.firmwareId
})
const queryFormRef = ref()
//
const taskFormRef = ref()
const queryFormRef = ref() //
const taskFormRef = ref() //
const taskDetailRef = ref() //
/** 获取任务列表 */
const getTaskList = async () => {
@ -138,6 +154,19 @@ const openTaskForm = () => {
taskFormRef.value?.open()
}
/** 处理任务创建成功 */
const handleTaskSuccess = () => {
getTaskList()
emit('success')
}
/** 查看任务详情 */
const handleViewDetail = (id: number | undefined) => {
if (id) {
taskDetailRef.value?.open(id)
}
}
/** 取消任务 */
const handleCancelTask = async (id: number) => {
try {

View File

@ -154,7 +154,7 @@ export const IoTOtaTaskDeviceScopeEnum = {
label: '全部设备',
value: 1
},
SPECIFIC: {
SELECT: {
label: '指定设备',
value: 2
}