feat(iot):优化 ota 的代码风格
parent
76bed17ed9
commit
584370358e
|
|
@ -1,173 +0,0 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
|
||||||
|
|
||||||
/** 新增/修改固件的表单 */
|
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'id',
|
|
||||||
dependencies: {
|
|
||||||
triggerFields: [''],
|
|
||||||
show: () => false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'name',
|
|
||||||
label: '固件名称',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入固件名称',
|
|
||||||
},
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'productId',
|
|
||||||
label: '所属产品',
|
|
||||||
component: 'ApiSelect',
|
|
||||||
componentProps: {
|
|
||||||
api: getSimpleProductList,
|
|
||||||
labelField: 'name',
|
|
||||||
valueField: 'id',
|
|
||||||
placeholder: '请选择产品',
|
|
||||||
},
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'version',
|
|
||||||
label: '版本号',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入版本号',
|
|
||||||
},
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'description',
|
|
||||||
label: '固件描述',
|
|
||||||
component: 'Textarea',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入固件描述',
|
|
||||||
rows: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'fileUrl',
|
|
||||||
label: '固件文件',
|
|
||||||
component: 'Upload',
|
|
||||||
componentProps: {
|
|
||||||
maxCount: 1,
|
|
||||||
accept: '.bin,.hex,.zip',
|
|
||||||
},
|
|
||||||
rules: 'required',
|
|
||||||
help: '支持上传 .bin、.hex、.zip 格式的固件文件',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 列表的搜索表单 */
|
|
||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
fieldName: 'name',
|
|
||||||
label: '固件名称',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入固件名称',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'productId',
|
|
||||||
label: '产品',
|
|
||||||
component: 'ApiSelect',
|
|
||||||
componentProps: {
|
|
||||||
api: getSimpleProductList,
|
|
||||||
labelField: 'name',
|
|
||||||
valueField: 'id',
|
|
||||||
placeholder: '请选择产品',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'createTime',
|
|
||||||
label: '创建时间',
|
|
||||||
component: 'RangePicker',
|
|
||||||
componentProps: {
|
|
||||||
...getRangePickerDefaultProps(),
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 列表的字段 */
|
|
||||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'checkbox',
|
|
||||||
width: 50,
|
|
||||||
fixed: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: '固件编号',
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
title: '固件名称',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'version',
|
|
||||||
title: '版本号',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'productName',
|
|
||||||
title: '所属产品',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'description',
|
|
||||||
title: '固件描述',
|
|
||||||
minWidth: 200,
|
|
||||||
showOverflow: 'tooltip',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'fileSize',
|
|
||||||
title: '文件大小',
|
|
||||||
width: 120,
|
|
||||||
formatter: ({ cellValue }) => {
|
|
||||||
if (!cellValue) return '-';
|
|
||||||
const kb = cellValue / 1024;
|
|
||||||
if (kb < 1024) return `${kb.toFixed(2)} KB`;
|
|
||||||
return `${(kb / 1024).toFixed(2)} MB`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'status',
|
|
||||||
title: '状态',
|
|
||||||
width: 100,
|
|
||||||
formatter: ({ cellValue }) => {
|
|
||||||
return cellValue === 1 ? '启用' : '禁用';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
width: 180,
|
|
||||||
formatter: 'formatDateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
width: 160,
|
|
||||||
fixed: 'right',
|
|
||||||
slots: { default: 'actions' },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -129,11 +129,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
title: '固件描述',
|
title: '固件描述',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
|
// TODO @AI:你看看 vue3 + ep 是这么干的么?
|
||||||
{
|
{
|
||||||
field: 'productId',
|
field: 'productName',
|
||||||
title: '所属产品',
|
title: '所属产品',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
slots: { default: 'product' },
|
formatter: ({ row }) => row.productName || '未知产品',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'fileUrl',
|
field: 'fileUrl',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Card, Descriptions } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getOtaFirmware } from '#/api/iot/ota/firmware';
|
||||||
|
import { getOtaTaskRecordStatusStatistics } from '#/api/iot/ota/task/record';
|
||||||
|
|
||||||
|
import OtaTaskList from '../../modules/task/list.vue';
|
||||||
|
import UpgradeStatistics from '../../modules/upgrade-statistics.vue';
|
||||||
|
|
||||||
|
/** IoT OTA 固件详情 */
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const firmwareId = ref(Number(route.params.id));
|
||||||
|
const firmwareLoading = ref(false);
|
||||||
|
const firmware = ref<IoTOtaFirmwareApi.Firmware>({} as IoTOtaFirmwareApi.Firmware);
|
||||||
|
|
||||||
|
const firmwareStatisticsLoading = ref(false);
|
||||||
|
const firmwareStatistics = ref<Record<string, number>>({});
|
||||||
|
|
||||||
|
/** 获取固件信息 */
|
||||||
|
async function getFirmwareInfo() {
|
||||||
|
firmwareLoading.value = true;
|
||||||
|
try {
|
||||||
|
firmware.value = await getOtaFirmware(firmwareId.value);
|
||||||
|
} finally {
|
||||||
|
firmwareLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取升级统计 */
|
||||||
|
async function getStatistics() {
|
||||||
|
firmwareStatisticsLoading.value = true;
|
||||||
|
try {
|
||||||
|
firmwareStatistics.value = await getOtaTaskRecordStatusStatistics(
|
||||||
|
firmwareId.value,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
firmwareStatisticsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getFirmwareInfo();
|
||||||
|
getStatistics();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- 固件信息 -->
|
||||||
|
<!-- TODO @AI:是不是搞成 data.ts 那种方式 -->
|
||||||
|
<!-- TODO @AI:拆成一个独立的组件? -->
|
||||||
|
<Card title="固件信息" class="mb-5" :loading="firmwareLoading">
|
||||||
|
<Descriptions :column="3" bordered>
|
||||||
|
<Descriptions.Item label="固件名称">
|
||||||
|
{{ firmware?.name }}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="所属产品">
|
||||||
|
{{ firmware?.productName }}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="固件版本">
|
||||||
|
{{ firmware?.version }}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间">
|
||||||
|
{{
|
||||||
|
firmware?.createTime
|
||||||
|
? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="固件描述" :span="2">
|
||||||
|
{{ firmware?.description }}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 升级设备统计 -->
|
||||||
|
<!-- TODO @AI:是不是搞个 detail/modules/upgrade-statistics;挪个目录? -->
|
||||||
|
<UpgradeStatistics
|
||||||
|
:loading="firmwareStatisticsLoading"
|
||||||
|
:statistics="firmwareStatistics"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 任务管理 -->
|
||||||
|
<OtaTaskList
|
||||||
|
v-if="firmware?.productId"
|
||||||
|
:firmware-id="firmwareId"
|
||||||
|
:product-id="firmware.productId"
|
||||||
|
@success="getStatistics"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -13,11 +13,9 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
|
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import OtaFirmwareForm from '../modules/ota-firmware-form.vue';
|
import OtaFirmwareForm from '../modules/firmware-form.vue';
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTOtaFirmware' });
|
|
||||||
|
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
|
@ -104,19 +102,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
label: $t('ui.actionTitle.create', ['固件']),
|
label: $t('ui.actionTitle.create', ['固件']),
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
icon: ACTION_ICON.ADD,
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['iot:ota-firmware:create'],
|
||||||
onClick: handleCreate,
|
onClick: handleCreate,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 产品名称列 -->
|
|
||||||
<template #product="{ row }">
|
|
||||||
<span class="text-gray-700">{{ row.productName || '未知产品' }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 固件文件列 -->
|
<!-- 固件文件列 -->
|
||||||
<template #fileUrl="{ row }">
|
<template #fileUrl="{ row }">
|
||||||
|
<!-- TODO @AI:使用 TableAction 按钮? -->
|
||||||
<div
|
<div
|
||||||
v-if="row.fileUrl"
|
v-if="row.fileUrl"
|
||||||
class="inline-flex items-center gap-1.5 align-middle leading-none"
|
class="inline-flex items-center gap-1.5 align-middle leading-none"
|
||||||
|
|
@ -136,8 +130,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="text-gray-400">无文件</span>
|
<span v-else class="text-gray-400">无文件</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作列 -->
|
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
|
|
@ -145,12 +137,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
label: $t('common.detail'),
|
label: $t('common.detail'),
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: ACTION_ICON.VIEW,
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['iot:ota-firmware:query'],
|
||||||
onClick: handleDetail.bind(null, row),
|
onClick: handleDetail.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('common.edit'),
|
label: $t('common.edit'),
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: ACTION_ICON.EDIT,
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['iot:ota-firmware:update'],
|
||||||
onClick: handleEdit.bind(null, row),
|
onClick: handleEdit.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -158,6 +152,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
type: 'link',
|
type: 'link',
|
||||||
danger: true,
|
danger: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['iot:ota-firmware:delete'],
|
||||||
popConfirm: {
|
popConfirm: {
|
||||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
confirm: handleDelete.bind(null, row),
|
confirm: handleDelete.bind(null, row),
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|
||||||
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
|
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
|
||||||
import OtaFirmwareForm from './modules/ota-firmware-form.vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTOtaFirmware' });
|
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
|
||||||
connectedComponent: OtaFirmwareForm,
|
|
||||||
destroyOnClose: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 刷新表格 */
|
|
||||||
function handleRefresh() {
|
|
||||||
gridApi.query();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建固件 */
|
|
||||||
function handleCreate() {
|
|
||||||
formModalApi.setData(null).open();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 编辑固件 */
|
|
||||||
function handleEdit(row: IoTOtaFirmwareApi.Firmware) {
|
|
||||||
formModalApi.setData(row).open();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删除固件 */
|
|
||||||
async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
|
|
||||||
const hideLoading = message.loading({
|
|
||||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await deleteOtaFirmware(row.id as number);
|
|
||||||
message.success({
|
|
||||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
|
||||||
});
|
|
||||||
handleRefresh();
|
|
||||||
} finally {
|
|
||||||
hideLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
|
||||||
formOptions: {
|
|
||||||
schema: useGridFormSchema(),
|
|
||||||
},
|
|
||||||
gridOptions: {
|
|
||||||
columns: useGridColumns(),
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues) => {
|
|
||||||
return await getOtaFirmwarePage({
|
|
||||||
pageNo: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...formValues,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
isHover: true,
|
|
||||||
},
|
|
||||||
toolbarConfig: {
|
|
||||||
refresh: true,
|
|
||||||
search: true,
|
|
||||||
},
|
|
||||||
} as VxeTableGridOptions<IoTOtaFirmwareApi.Firmware>,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Page auto-content-height>
|
|
||||||
<FormModal @success="handleRefresh" />
|
|
||||||
<Grid table-title="OTA 固件列表">
|
|
||||||
<template #toolbar-tools>
|
|
||||||
<TableAction
|
|
||||||
:actions="[
|
|
||||||
{
|
|
||||||
label: $t('ui.actionTitle.create', ['固件']),
|
|
||||||
type: 'primary',
|
|
||||||
icon: ACTION_ICON.ADD,
|
|
||||||
auth: ['iot:ota-firmware:create'],
|
|
||||||
onClick: handleCreate,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #actions="{ row }">
|
|
||||||
<TableAction
|
|
||||||
:actions="[
|
|
||||||
{
|
|
||||||
label: $t('common.edit'),
|
|
||||||
type: 'link',
|
|
||||||
icon: ACTION_ICON.EDIT,
|
|
||||||
auth: ['iot:ota-firmware:update'],
|
|
||||||
onClick: handleEdit.bind(null, row),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: $t('common.delete'),
|
|
||||||
type: 'link',
|
|
||||||
danger: true,
|
|
||||||
icon: ACTION_ICON.DELETE,
|
|
||||||
auth: ['iot:ota-firmware:delete'],
|
|
||||||
popConfirm: {
|
|
||||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
|
||||||
confirm: handleDelete.bind(null, row),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Grid>
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
import { formatDate } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getOtaFirmware } from '#/api/iot/ota/firmware';
|
|
||||||
import { getOtaTaskRecordStatusStatistics } from '#/api/iot/ota/task/record';
|
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import OtaTaskList from '../task/ota-task-list.vue';
|
|
||||||
|
|
||||||
/** IoT OTA 固件详情 */
|
|
||||||
defineOptions({ name: 'IoTOtaFirmwareDetail' });
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const firmwareId = ref(Number(route.params.id));
|
|
||||||
const firmwareLoading = ref(false);
|
|
||||||
const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware);
|
|
||||||
|
|
||||||
const firmwareStatisticsLoading = ref(false);
|
|
||||||
const firmwareStatistics = ref<Record<string, number>>({});
|
|
||||||
|
|
||||||
/** 获取固件信息 */
|
|
||||||
async function getFirmwareInfo() {
|
|
||||||
firmwareLoading.value = true;
|
|
||||||
try {
|
|
||||||
firmware.value = await getOtaFirmware(firmwareId.value);
|
|
||||||
} finally {
|
|
||||||
firmwareLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取升级统计 */
|
|
||||||
async function getStatistics() {
|
|
||||||
firmwareStatisticsLoading.value = true;
|
|
||||||
try {
|
|
||||||
firmwareStatistics.value = await getOtaTaskRecordStatusStatistics(
|
|
||||||
firmwareId.value,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
firmwareStatisticsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
getFirmwareInfo();
|
|
||||||
getStatistics();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-4">
|
|
||||||
<!-- 固件信息 -->
|
|
||||||
<Card title="固件信息" class="mb-3" :loading="firmwareLoading">
|
|
||||||
<Descriptions :column="3" bordered size="small">
|
|
||||||
<Descriptions.Item label="固件名称">
|
|
||||||
{{ firmware?.name }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="所属产品">
|
|
||||||
{{ firmware?.productName }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="固件版本">
|
|
||||||
{{ firmware?.version }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="创建时间">
|
|
||||||
{{
|
|
||||||
firmware?.createTime
|
|
||||||
? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
: '-'
|
|
||||||
}}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="固件描述" :span="2">
|
|
||||||
{{ firmware?.description }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 升级设备统计 -->
|
|
||||||
<Card
|
|
||||||
title="升级设备统计"
|
|
||||||
class="mb-3"
|
|
||||||
:loading="firmwareStatisticsLoading"
|
|
||||||
>
|
|
||||||
<Row :gutter="20" class="py-3">
|
|
||||||
<Col :span="6">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-blue-500">
|
|
||||||
{{
|
|
||||||
Object.values(firmwareStatistics).reduce(
|
|
||||||
(sum: number, count) => sum + (count || 0),
|
|
||||||
0,
|
|
||||||
) || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级设备总数</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-gray-400">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">待推送</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-blue-400">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">已推送</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-yellow-500">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[
|
|
||||||
IoTOtaTaskRecordStatusEnum.UPGRADING.value
|
|
||||||
] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">正在升级</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-green-500">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级成功</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-red-500">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级失败</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-1 text-3xl font-bold text-gray-400">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级取消</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 任务管理 -->
|
|
||||||
<OtaTaskList
|
|
||||||
v-if="firmware?.productId"
|
|
||||||
:firmware-id="firmwareId"
|
|
||||||
:product-id="firmware.productId"
|
|
||||||
@success="getStatistics"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
import { formatDate } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getOtaFirmware } from '#/api/iot/ota/firmware';
|
|
||||||
import { getOtaTaskRecordStatusStatistics } from '#/api/iot/ota/task/record';
|
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import OtaTaskList from '../task/ota-task-list.vue';
|
|
||||||
|
|
||||||
/** IoT OTA 固件详情 */
|
|
||||||
defineOptions({ name: 'IoTOtaFirmwareDetail' });
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const firmwareId = ref(Number(route.params.id));
|
|
||||||
const firmwareLoading = ref(false);
|
|
||||||
const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware);
|
|
||||||
|
|
||||||
const firmwareStatisticsLoading = ref(false);
|
|
||||||
const firmwareStatistics = ref<Record<string, number>>({});
|
|
||||||
|
|
||||||
/** 获取固件信息 */
|
|
||||||
async function getFirmwareInfo() {
|
|
||||||
firmwareLoading.value = true;
|
|
||||||
try {
|
|
||||||
firmware.value = await getOtaFirmware(firmwareId.value);
|
|
||||||
} finally {
|
|
||||||
firmwareLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取升级统计 */
|
|
||||||
async function getStatistics() {
|
|
||||||
firmwareStatisticsLoading.value = true;
|
|
||||||
try {
|
|
||||||
firmwareStatistics.value = await getOtaTaskRecordStatusStatistics(
|
|
||||||
firmwareId.value,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
firmwareStatisticsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
getFirmwareInfo();
|
|
||||||
getStatistics();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-4">
|
|
||||||
<!-- 固件信息 -->
|
|
||||||
<Card title="固件信息" class="mb-5" :loading="firmwareLoading">
|
|
||||||
<Descriptions :column="3" bordered>
|
|
||||||
<Descriptions.Item label="固件名称">
|
|
||||||
{{ firmware?.name }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="所属产品">
|
|
||||||
{{ firmware?.productName }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="固件版本">
|
|
||||||
{{ firmware?.version }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="创建时间">
|
|
||||||
{{
|
|
||||||
firmware?.createTime
|
|
||||||
? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
: '-'
|
|
||||||
}}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="固件描述" :span="2">
|
|
||||||
{{ firmware?.description }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 升级设备统计 -->
|
|
||||||
<Card
|
|
||||||
title="升级设备统计"
|
|
||||||
class="mb-5"
|
|
||||||
:loading="firmwareStatisticsLoading"
|
|
||||||
>
|
|
||||||
<Row :gutter="20" class="py-5">
|
|
||||||
<Col :span="6">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-blue-500">
|
|
||||||
{{
|
|
||||||
Object.values(firmwareStatistics).reduce(
|
|
||||||
(sum: number, count) => sum + (count || 0),
|
|
||||||
0,
|
|
||||||
) || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级设备总数</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-gray-400">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">待推送</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-blue-400">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">已推送</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-yellow-500">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[
|
|
||||||
IoTOtaTaskRecordStatusEnum.UPGRADING.value
|
|
||||||
] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">正在升级</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-green-500">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级成功</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-red-500">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级失败</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-gray-400">
|
|
||||||
{{
|
|
||||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级取消</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 任务管理 -->
|
|
||||||
<OtaTaskList
|
|
||||||
v-if="firmware?.productId"
|
|
||||||
:firmware-id="firmwareId"
|
|
||||||
:product-id="firmware.productId"
|
|
||||||
@success="getStatistics"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// TODO @AI:是不是要挪到 firmware/modules/form.vue?
|
||||||
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
// TODO @AI:将 task 整个挪出去?放到 /Users/yunai/Java/yudao-all-in-vben/yudao-ui-admin-vben/apps/web-antd/src/views/iot/ota/task;
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** 新增升级任务的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'firmwareId',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '任务名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入任务名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'description',
|
||||||
|
label: '任务描述',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入任务描述',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'deviceScope',
|
||||||
|
label: '升级范围',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE, 'number'),
|
||||||
|
placeholder: '请选择升级范围',
|
||||||
|
},
|
||||||
|
defaultValue: IoTOtaTaskDeviceScopeEnum.ALL.value,
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'deviceIds',
|
||||||
|
label: '选择设备',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'multiple',
|
||||||
|
placeholder: '请选择设备',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: true,
|
||||||
|
optionFilterProp: 'label',
|
||||||
|
},
|
||||||
|
defaultValue: [],
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['deviceScope'],
|
||||||
|
show: (values) =>
|
||||||
|
values.deviceScope === IoTOtaTaskDeviceScopeEnum.SELECT.value,
|
||||||
|
rules: (values) =>
|
||||||
|
values.deviceScope === IoTOtaTaskDeviceScopeEnum.SELECT.value
|
||||||
|
? 'required'
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '任务名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入任务名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '任务编号',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '任务名称',
|
||||||
|
minWidth: 150,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deviceScope',
|
||||||
|
title: '升级范围',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'progress',
|
||||||
|
title: '升级进度',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
formatter: ({ row }) =>
|
||||||
|
`${row.deviceSuccessCount || 0}/${row.deviceTotalCount || 0}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '任务描述',
|
||||||
|
minWidth: 150,
|
||||||
|
align: 'center',
|
||||||
|
showOverflow: 'tooltip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '任务状态',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.IOT_OTA_TASK_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 升级记录的列表字段 */
|
||||||
|
export function useRecordGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'deviceName',
|
||||||
|
title: '设备名称',
|
||||||
|
minWidth: 150,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fromFirmwareVersion',
|
||||||
|
title: '当前版本',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '升级状态',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.IOT_OTA_TASK_RECORD_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'progress',
|
||||||
|
title: '升级进度',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
formatter: ({ row }) => `${row.progress || 0}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '状态描述',
|
||||||
|
minWidth: 150,
|
||||||
|
align: 'center',
|
||||||
|
showOverflow: 'tooltip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updateTime',
|
||||||
|
title: '更新时间',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
||||||
|
import type { IoTOtaTaskRecordApi } from '#/api/iot/ota/task/record';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Card, Descriptions, message, Tabs, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getOtaTask } from '#/api/iot/ota/task';
|
||||||
|
import {
|
||||||
|
cancelOtaTaskRecord,
|
||||||
|
getOtaTaskRecordPage,
|
||||||
|
getOtaTaskRecordStatusStatistics,
|
||||||
|
} from '#/api/iot/ota/task/record';
|
||||||
|
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import UpgradeStatistics from '../upgrade-statistics.vue';
|
||||||
|
import { useRecordGridColumns } from './data';
|
||||||
|
|
||||||
|
/** OTA 任务详情组件 */
|
||||||
|
defineOptions({ name: 'IoTOtaTaskDetail' });
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const taskId = ref<number>();
|
||||||
|
const taskLoading = ref(false);
|
||||||
|
const task = ref<IoTOtaTaskApi.Task>({} as IoTOtaTaskApi.Task);
|
||||||
|
|
||||||
|
const taskStatisticsLoading = ref(false);
|
||||||
|
const taskStatistics = ref<Record<string, number>>({});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取任务详情 */
|
||||||
|
async function getTaskInfo() {
|
||||||
|
if (!taskId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskLoading.value = true;
|
||||||
|
try {
|
||||||
|
task.value = await getOtaTask(taskId.value);
|
||||||
|
} finally {
|
||||||
|
taskLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取统计数据 */
|
||||||
|
async function getStatistics() {
|
||||||
|
if (!taskId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskStatisticsLoading.value = true;
|
||||||
|
try {
|
||||||
|
taskStatistics.value = await getOtaTaskRecordStatusStatistics(
|
||||||
|
undefined,
|
||||||
|
taskId.value,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
taskStatisticsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换标签 */
|
||||||
|
async function handleTabChange(tabKey: number | string) {
|
||||||
|
activeTab.value = String(tabKey);
|
||||||
|
await gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消升级 */
|
||||||
|
async function handleCancelUpgrade(record: IoTOtaTaskRecordApi.TaskRecord) {
|
||||||
|
await cancelOtaTaskRecord(record.id as number);
|
||||||
|
message.success('取消成功');
|
||||||
|
await gridApi.query();
|
||||||
|
await getStatistics();
|
||||||
|
await getTaskInfo();
|
||||||
|
emit('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useRecordGridColumns(),
|
||||||
|
height: 400,
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }) => {
|
||||||
|
if (!taskId.value) {
|
||||||
|
return { list: [], total: 0 };
|
||||||
|
}
|
||||||
|
return await getOtaTaskRecordPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
taskId: taskId.value,
|
||||||
|
status: activeTab.value === '' ? undefined : Number(activeTab.value),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<IoTOtaTaskRecordApi.TaskRecord>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<{ id: number }>();
|
||||||
|
if (!data?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskId.value = data.id;
|
||||||
|
activeTab.value = '';
|
||||||
|
await Promise.all([getTaskInfo(), getStatistics()]);
|
||||||
|
await gridApi.query();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="升级任务详情" class="w-5/6" :show-cancel-button="false" :show-confirm-button="false">
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- 任务信息 -->
|
||||||
|
<Card title="任务信息" class="mb-5" :loading="taskLoading">
|
||||||
|
<Descriptions :column="3" bordered>
|
||||||
|
<Descriptions.Item label="任务编号">{{ task.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="任务名称">
|
||||||
|
{{ task.name }}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="升级范围">
|
||||||
|
<Tag>
|
||||||
|
{{ getDictLabel(DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE, task.deviceScope) }}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="任务状态">
|
||||||
|
<Tag>
|
||||||
|
{{ getDictLabel(DICT_TYPE.IOT_OTA_TASK_STATUS, task.status) }}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间">
|
||||||
|
{{
|
||||||
|
task.createTime
|
||||||
|
? formatDate(task.createTime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="任务描述" :span="3">
|
||||||
|
{{ task.description }}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 任务升级设备统计 -->
|
||||||
|
<UpgradeStatistics
|
||||||
|
:loading="taskStatisticsLoading"
|
||||||
|
:statistics="taskStatistics"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 设备管理 -->
|
||||||
|
<Card title="升级设备记录">
|
||||||
|
<Tabs
|
||||||
|
v-model:active-key="activeTab"
|
||||||
|
@change="handleTabChange"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<Tabs.TabPane
|
||||||
|
v-for="tab in statusTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
:tab="tab.label"
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
<Grid>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '取消',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
ifShow: [
|
||||||
|
IoTOtaTaskRecordStatusEnum.PENDING.value,
|
||||||
|
IoTOtaTaskRecordStatusEnum.PUSHED.value,
|
||||||
|
IoTOtaTaskRecordStatusEnum.UPGRADING.value,
|
||||||
|
].includes(row.status),
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认要取消该设备的升级任务吗?',
|
||||||
|
confirm: handleCancelUpgrade.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { getDeviceListByProductId } from '#/api/iot/device/device';
|
||||||
|
import { createOtaTask } from '#/api/iot/ota/task';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from './data';
|
||||||
|
|
||||||
|
/** IoT OTA 升级任务表单 */
|
||||||
|
defineOptions({ name: 'IoTOtaTaskForm' });
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const data = (await formApi.getValues()) as IoTOtaTaskApi.Task;
|
||||||
|
await createOtaTask(data);
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<{ firmwareId: number; productId: number }>();
|
||||||
|
if (!data?.firmwareId || !data?.productId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
// 设置 firmwareId
|
||||||
|
await formApi.setValues({ firmwareId: data.firmwareId });
|
||||||
|
// 加载产品下的设备列表
|
||||||
|
const devices = (await getDeviceListByProductId(data.productId)) || [];
|
||||||
|
formApi.updateSchema([
|
||||||
|
{
|
||||||
|
fieldName: 'deviceIds',
|
||||||
|
componentProps: {
|
||||||
|
options: devices.map((device) => ({
|
||||||
|
label: device.nickname
|
||||||
|
? `${device.deviceName} (${device.nickname})`
|
||||||
|
: device.deviceName,
|
||||||
|
value: device.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-3/5" title="新增升级任务">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
||||||
|
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import OtaTaskDetail from './detail.vue';
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import OtaTaskForm from './form.vue';
|
||||||
|
|
||||||
|
/** IoT OTA 任务列表 */
|
||||||
|
defineOptions({ name: 'IoTOtaTaskList' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
firmwareId: number;
|
||||||
|
productId: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: OtaTaskForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: OtaTaskDetail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
async function handleRefresh() {
|
||||||
|
await gridApi.query();
|
||||||
|
emit('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增任务 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi
|
||||||
|
.setData({ firmwareId: props.firmwareId, productId: props.productId })
|
||||||
|
.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看任务详情 */
|
||||||
|
function handleDetail(row: IoTOtaTaskApi.Task) {
|
||||||
|
detailModalApi.setData({ id: row.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消任务 */
|
||||||
|
async function handleCancel(row: IoTOtaTaskApi.Task) {
|
||||||
|
await cancelOtaTask(row.id as number);
|
||||||
|
message.success('取消成功');
|
||||||
|
await handleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getOtaTaskPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
firmwareId: props.firmwareId,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<IoTOtaTaskApi.Task>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="升级任务管理">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '详情',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '取消',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
ifShow: row.status === IoTOtaTaskStatusEnum.IN_PROGRESS.value,
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认要取消该升级任务吗?',
|
||||||
|
confirm: handleCancel.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,415 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { TableColumnsType } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import type { OtaTask } from '#/api/iot/ota/task';
|
|
||||||
import type { OtaTaskRecord } from '#/api/iot/ota/task/record';
|
|
||||||
|
|
||||||
import { computed, reactive, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
|
||||||
import { formatDate } from '@vben/utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Descriptions,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Row,
|
|
||||||
Table,
|
|
||||||
Tabs,
|
|
||||||
Tag,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getOtaTask } from '#/api/iot/ota/task';
|
|
||||||
import {
|
|
||||||
cancelOtaTaskRecord,
|
|
||||||
getOtaTaskRecordPage,
|
|
||||||
getOtaTaskRecordStatusStatistics,
|
|
||||||
} from '#/api/iot/ota/task/record';
|
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** OTA 任务详情组件 */
|
|
||||||
defineOptions({ name: 'OtaTaskDetail' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
|
|
||||||
const taskId = ref<number>();
|
|
||||||
const taskLoading = ref(false);
|
|
||||||
const task = ref<OtaTask>({} as OtaTask);
|
|
||||||
|
|
||||||
const taskStatisticsLoading = ref(false);
|
|
||||||
const taskStatistics = ref<Record<string, number>>({});
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
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 columns: TableColumnsType = [
|
|
||||||
{
|
|
||||||
title: '设备名称',
|
|
||||||
dataIndex: 'deviceName',
|
|
||||||
key: 'deviceName',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '当前版本',
|
|
||||||
dataIndex: 'fromFirmwareVersion',
|
|
||||||
key: 'fromFirmwareVersion',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '升级状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
align: 'center' as const,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '升级进度',
|
|
||||||
dataIndex: 'progress',
|
|
||||||
key: 'progress',
|
|
||||||
align: 'center' as const,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态描述',
|
|
||||||
dataIndex: 'description',
|
|
||||||
key: 'description',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
dataIndex: 'updateTime',
|
|
||||||
key: 'updateTime',
|
|
||||||
align: 'center' as const,
|
|
||||||
width: 180,
|
|
||||||
customRender: ({ text }: any) => formatDate(text, 'YYYY-MM-DD HH:mm:ss'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
align: 'center' as const,
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [ModalComponent, modalApi] = useVbenModal();
|
|
||||||
|
|
||||||
/** 获取任务详情 */
|
|
||||||
async function getTaskInfo() {
|
|
||||||
if (!taskId.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
taskLoading.value = true;
|
|
||||||
try {
|
|
||||||
task.value = await getOtaTask(taskId.value);
|
|
||||||
} finally {
|
|
||||||
taskLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取统计数据 */
|
|
||||||
async function getStatistics() {
|
|
||||||
if (!taskId.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
taskStatisticsLoading.value = true;
|
|
||||||
try {
|
|
||||||
taskStatistics.value = await getOtaTaskRecordStatusStatistics(
|
|
||||||
undefined,
|
|
||||||
taskId.value,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
taskStatisticsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取升级记录列表 */
|
|
||||||
async function getRecordList() {
|
|
||||||
if (!taskId.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
recordLoading.value = true;
|
|
||||||
try {
|
|
||||||
queryParams.taskId = taskId.value;
|
|
||||||
const data = await getOtaTaskRecordPage(queryParams);
|
|
||||||
recordList.value = data.list || [];
|
|
||||||
recordTotal.value = data.total || 0;
|
|
||||||
} finally {
|
|
||||||
recordLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 切换标签 */
|
|
||||||
function handleTabChange(tabKey: number | string) {
|
|
||||||
activeTab.value = String(tabKey);
|
|
||||||
queryParams.pageNo = 1;
|
|
||||||
queryParams.status =
|
|
||||||
activeTab.value === '' ? undefined : Number.parseInt(String(tabKey));
|
|
||||||
getRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 分页变化 */
|
|
||||||
function handleTableChange(pagination: any) {
|
|
||||||
queryParams.pageNo = pagination.current;
|
|
||||||
queryParams.pageSize = pagination.pageSize;
|
|
||||||
getRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取消升级 */
|
|
||||||
async function handleCancelUpgrade(record: OtaTaskRecord) {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认取消',
|
|
||||||
content: '确认要取消该设备的升级任务吗?',
|
|
||||||
async onOk() {
|
|
||||||
try {
|
|
||||||
await cancelOtaTaskRecord(record.id!);
|
|
||||||
message.success('取消成功');
|
|
||||||
await getRecordList();
|
|
||||||
await getStatistics();
|
|
||||||
await getTaskInfo();
|
|
||||||
emit('success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消升级失败', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
|
||||||
function open(id: number) {
|
|
||||||
modalApi.open();
|
|
||||||
taskId.value = id;
|
|
||||||
activeTab.value = '';
|
|
||||||
queryParams.pageNo = 1;
|
|
||||||
queryParams.status = undefined;
|
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
getTaskInfo();
|
|
||||||
getStatistics();
|
|
||||||
getRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 暴露方法 */
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ModalComponent title="升级任务详情" class="w-5/6">
|
|
||||||
<div class="p-4">
|
|
||||||
<!-- 任务信息 -->
|
|
||||||
<Card title="任务信息" class="mb-5" :loading="taskLoading">
|
|
||||||
<Descriptions :column="3" bordered>
|
|
||||||
<Descriptions.Item label="任务编号">{{ task.id }}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="任务名称">
|
|
||||||
{{ task.name }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="升级范围">
|
|
||||||
<Tag v-if="task.deviceScope === 1" color="blue">全部设备</Tag>
|
|
||||||
<Tag v-else-if="task.deviceScope === 2" color="green">指定设备</Tag>
|
|
||||||
<Tag v-else>{{ task.deviceScope }}</Tag>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="任务状态">
|
|
||||||
<Tag v-if="task.status === 0" color="orange">待执行</Tag>
|
|
||||||
<Tag v-else-if="task.status === 1" color="blue">执行中</Tag>
|
|
||||||
<Tag v-else-if="task.status === 2" color="green">已完成</Tag>
|
|
||||||
<Tag v-else-if="task.status === 3" color="red">已取消</Tag>
|
|
||||||
<Tag v-else>{{ task.status }}</Tag>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="创建时间">
|
|
||||||
{{
|
|
||||||
task.createTime
|
|
||||||
? formatDate(task.createTime, 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
: '-'
|
|
||||||
}}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="任务描述" :span="3">
|
|
||||||
{{ task.description }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 任务升级设备统计 -->
|
|
||||||
<Card title="升级设备统计" class="mb-5" :loading="taskStatisticsLoading">
|
|
||||||
<Row :gutter="20" class="py-5">
|
|
||||||
<Col :span="6">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-blue-500">
|
|
||||||
{{
|
|
||||||
Object.values(taskStatistics).reduce(
|
|
||||||
(sum, count) => sum + (count || 0),
|
|
||||||
0,
|
|
||||||
) || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级设备总数</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-gray-400">
|
|
||||||
{{
|
|
||||||
taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">待推送</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-blue-400">
|
|
||||||
{{
|
|
||||||
taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">已推送</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-yellow-500">
|
|
||||||
{{
|
|
||||||
taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] ||
|
|
||||||
0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">正在升级</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-green-500">
|
|
||||||
{{
|
|
||||||
taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级成功</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-red-500">
|
|
||||||
{{
|
|
||||||
taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级失败</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col :span="3">
|
|
||||||
<div
|
|
||||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
|
||||||
>
|
|
||||||
<div class="mb-2 text-3xl font-bold text-gray-400">
|
|
||||||
{{
|
|
||||||
taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-600">升级取消</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 设备管理 -->
|
|
||||||
<Card title="升级设备记录">
|
|
||||||
<Tabs
|
|
||||||
v-model:active-key="activeTab"
|
|
||||||
@change="handleTabChange"
|
|
||||||
class="mb-4"
|
|
||||||
>
|
|
||||||
<Tabs.TabPane
|
|
||||||
v-for="tab in statusTabs"
|
|
||||||
:key="tab.key"
|
|
||||||
:tab="tab.label"
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
:columns="columns"
|
|
||||||
:data-source="recordList"
|
|
||||||
:loading="recordLoading"
|
|
||||||
:pagination="{
|
|
||||||
current: queryParams.pageNo,
|
|
||||||
pageSize: queryParams.pageSize,
|
|
||||||
total: recordTotal,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
showTotal: (total: number) => `共 ${total} 条`,
|
|
||||||
}"
|
|
||||||
@change="handleTableChange"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, record }">
|
|
||||||
<!-- 升级状态 -->
|
|
||||||
<template v-if="column.key === 'status'">
|
|
||||||
<Tag v-if="record.status === 0" color="default">待推送</Tag>
|
|
||||||
<Tag v-else-if="record.status === 1" color="blue">已推送</Tag>
|
|
||||||
<Tag v-else-if="record.status === 2" color="processing">
|
|
||||||
升级中
|
|
||||||
</Tag>
|
|
||||||
<Tag v-else-if="record.status === 3" color="success">成功</Tag>
|
|
||||||
<Tag v-else-if="record.status === 4" color="error">失败</Tag>
|
|
||||||
<Tag v-else-if="record.status === 5" color="warning">已取消</Tag>
|
|
||||||
<Tag v-else>{{ record.status }}</Tag>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 升级进度 -->
|
|
||||||
<template v-else-if="column.key === 'progress'">
|
|
||||||
{{ record.progress }}%
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 操作 -->
|
|
||||||
<template v-else-if="column.key === 'action'">
|
|
||||||
<a
|
|
||||||
v-if="
|
|
||||||
[
|
|
||||||
IoTOtaTaskRecordStatusEnum.PENDING.value,
|
|
||||||
IoTOtaTaskRecordStatusEnum.PUSHED.value,
|
|
||||||
IoTOtaTaskRecordStatusEnum.UPGRADING.value,
|
|
||||||
].includes(record.status)
|
|
||||||
"
|
|
||||||
class="text-red-500"
|
|
||||||
@click="handleCancelUpgrade(record)"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</ModalComponent>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
|
||||||
import type { OtaTask } from '#/api/iot/ota/task';
|
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { Form, Input, message, Select, Spin } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getDeviceListByProductId } from '#/api/iot/device/device';
|
|
||||||
import { createOtaTask } from '#/api/iot/ota/task';
|
|
||||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** IoT OTA 升级任务表单 */
|
|
||||||
defineOptions({ name: 'OtaTaskForm' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
firmwareId: number;
|
|
||||||
productId: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
|
|
||||||
const formLoading = ref(false);
|
|
||||||
const formData = ref<OtaTask>({
|
|
||||||
name: '',
|
|
||||||
deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
|
|
||||||
firmwareId: props.firmwareId,
|
|
||||||
description: '',
|
|
||||||
deviceIds: [],
|
|
||||||
});
|
|
||||||
const formRef = ref();
|
|
||||||
const formRules = {
|
|
||||||
name: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入任务名称',
|
|
||||||
trigger: 'blur' as const,
|
|
||||||
type: 'string' as const,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
deviceScope: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择升级范围',
|
|
||||||
trigger: 'change' as const,
|
|
||||||
type: 'number' as const,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
deviceIds: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请至少选择一个设备',
|
|
||||||
trigger: 'change' as const,
|
|
||||||
type: 'array' as const,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const devices = ref<IotDeviceApi.Device[]>([]);
|
|
||||||
|
|
||||||
/** 设备选项 */
|
|
||||||
const deviceOptions = computed(() => {
|
|
||||||
return devices.value.map((device) => ({
|
|
||||||
label: device.nickname
|
|
||||||
? `${device.deviceName} (${device.nickname})`
|
|
||||||
: device.deviceName,
|
|
||||||
value: device.id,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 升级范围选项 */
|
|
||||||
const deviceScopeOptions = computed(() => {
|
|
||||||
return Object.values(IoTOtaTaskDeviceScopeEnum).map((item) => ({
|
|
||||||
label: item.label,
|
|
||||||
value: item.value,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
|
||||||
async onConfirm() {
|
|
||||||
try {
|
|
||||||
await formRef.value.validate();
|
|
||||||
modalApi.lock();
|
|
||||||
await createOtaTask(formData.value);
|
|
||||||
message.success('创建成功');
|
|
||||||
await modalApi.close();
|
|
||||||
emit('success');
|
|
||||||
} finally {
|
|
||||||
modalApi.unlock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async onOpenChange(isOpen: boolean) {
|
|
||||||
if (!isOpen) {
|
|
||||||
resetForm();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 加载设备列表
|
|
||||||
formLoading.value = true;
|
|
||||||
try {
|
|
||||||
devices.value = (await getDeviceListByProductId(props.productId)) || [];
|
|
||||||
} finally {
|
|
||||||
formLoading.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 重置表单 */
|
|
||||||
function resetForm() {
|
|
||||||
formData.value = {
|
|
||||||
name: '',
|
|
||||||
deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
|
|
||||||
firmwareId: props.firmwareId,
|
|
||||||
description: '',
|
|
||||||
deviceIds: [],
|
|
||||||
};
|
|
||||||
formRef.value?.resetFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
|
||||||
async function open() {
|
|
||||||
await modalApi.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal title="新增升级任务" class="w-3/5">
|
|
||||||
<Spin :spinning="formLoading">
|
|
||||||
<Form
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="formRules"
|
|
||||||
:label-col="{ span: 4 }"
|
|
||||||
:wrapper-col="{ span: 20 }"
|
|
||||||
class="mx-4"
|
|
||||||
>
|
|
||||||
<Form.Item label="任务名称" name="name">
|
|
||||||
<Input v-model:value="formData.name" placeholder="请输入任务名称" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="任务描述" name="description">
|
|
||||||
<Input.TextArea
|
|
||||||
v-model:value="formData.description"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="请输入任务描述"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="升级范围" name="deviceScope">
|
|
||||||
<Select
|
|
||||||
v-model:value="formData.deviceScope"
|
|
||||||
placeholder="请选择升级范围"
|
|
||||||
:options="deviceScopeOptions"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
v-if="formData.deviceScope === IoTOtaTaskDeviceScopeEnum.SELECT.value"
|
|
||||||
label="选择设备"
|
|
||||||
name="deviceIds"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
v-model:value="formData.deviceIds"
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="请选择设备"
|
|
||||||
:options="deviceOptions"
|
|
||||||
:filter-option="true"
|
|
||||||
show-search
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Spin>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { TableColumnsType } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import type { OtaTask } from '#/api/iot/ota/task';
|
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
import { formatDate } from '@vben/utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Input,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getOtaTaskPage } from '#/api/iot/ota/task';
|
|
||||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import OtaTaskDetail from './ota-task-detail.vue';
|
|
||||||
import OtaTaskForm from './ota-task-form.vue';
|
|
||||||
|
|
||||||
/** IoT OTA 任务列表 */
|
|
||||||
defineOptions({ name: 'OtaTaskList' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
firmwareId: number;
|
|
||||||
productId: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
|
|
||||||
// 任务列表
|
|
||||||
const taskLoading = ref(false);
|
|
||||||
const taskList = ref<OtaTask[]>([]);
|
|
||||||
const taskTotal = ref(0);
|
|
||||||
const queryParams = reactive({
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
name: '',
|
|
||||||
firmwareId: props.firmwareId,
|
|
||||||
});
|
|
||||||
const taskFormRef = ref(); // 任务表单引用
|
|
||||||
const taskDetailRef = ref(); // 任务详情引用
|
|
||||||
|
|
||||||
/** 获取任务列表 */
|
|
||||||
async function getTaskList() {
|
|
||||||
taskLoading.value = true;
|
|
||||||
try {
|
|
||||||
const data = await getOtaTaskPage(queryParams);
|
|
||||||
taskList.value = data.list;
|
|
||||||
taskTotal.value = data.total;
|
|
||||||
} finally {
|
|
||||||
taskLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索 */
|
|
||||||
function handleQuery() {
|
|
||||||
queryParams.pageNo = 1;
|
|
||||||
getTaskList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打开任务表单 */
|
|
||||||
function openTaskForm() {
|
|
||||||
taskFormRef.value?.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理任务创建成功 */
|
|
||||||
function handleTaskCreateSuccess() {
|
|
||||||
getTaskList();
|
|
||||||
emit('success');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 查看任务详情 */
|
|
||||||
function handleTaskDetail(id: number) {
|
|
||||||
taskDetailRef.value?.open(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取消任务 */
|
|
||||||
async function handleCancelTask(id: number) {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认取消',
|
|
||||||
content: '确认要取消该升级任务吗?',
|
|
||||||
async onOk() {
|
|
||||||
try {
|
|
||||||
await IoTOtaTaskApi.cancelOtaTask(id);
|
|
||||||
message.success('取消成功');
|
|
||||||
await refresh();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消任务失败', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 刷新数据 */
|
|
||||||
async function refresh() {
|
|
||||||
await getTaskList();
|
|
||||||
emit('success');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 分页变化 */
|
|
||||||
function handleTableChange(pagination: any) {
|
|
||||||
queryParams.pageNo = pagination.current;
|
|
||||||
queryParams.pageSize = pagination.pageSize;
|
|
||||||
getTaskList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 表格列配置 */
|
|
||||||
const columns: TableColumnsType = [
|
|
||||||
{
|
|
||||||
title: '任务编号',
|
|
||||||
dataIndex: 'id',
|
|
||||||
key: 'id',
|
|
||||||
width: 80,
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '任务名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '升级范围',
|
|
||||||
dataIndex: 'deviceScope',
|
|
||||||
key: 'deviceScope',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '升级进度',
|
|
||||||
key: 'progress',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'createTime',
|
|
||||||
key: 'createTime',
|
|
||||||
align: 'center' as const,
|
|
||||||
customRender: ({ text }: any) => formatDate(text, 'YYYY-MM-DD HH:mm:ss'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '任务描述',
|
|
||||||
dataIndex: 'description',
|
|
||||||
key: 'description',
|
|
||||||
align: 'center' as const,
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '任务状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
align: 'center' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
align: 'center' as const,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
getTaskList();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card title="升级任务管理" class="mb-5">
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div class="mb-4 flex items-center justify-between">
|
|
||||||
<Button type="primary" @click="openTaskForm">
|
|
||||||
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
|
||||||
新增
|
|
||||||
</Button>
|
|
||||||
<Input
|
|
||||||
v-model:value="queryParams.name"
|
|
||||||
placeholder="请输入任务名称"
|
|
||||||
allow-clear
|
|
||||||
@press-enter="handleQuery"
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 任务列表 -->
|
|
||||||
<Table
|
|
||||||
:columns="columns"
|
|
||||||
:data-source="taskList"
|
|
||||||
:loading="taskLoading"
|
|
||||||
:pagination="{
|
|
||||||
current: queryParams.pageNo,
|
|
||||||
pageSize: queryParams.pageSize,
|
|
||||||
total: taskTotal,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
showTotal: (total: number) => `共 ${total} 条`,
|
|
||||||
}"
|
|
||||||
:scroll="{ x: 'max-content' }"
|
|
||||||
@change="handleTableChange"
|
|
||||||
>
|
|
||||||
<!-- 升级范围 -->
|
|
||||||
<template #bodyCell="{ column, record }">
|
|
||||||
<template v-if="column.key === 'deviceScope'">
|
|
||||||
<Tag v-if="record.deviceScope === 1" color="blue">全部设备</Tag>
|
|
||||||
<Tag v-else-if="record.deviceScope === 2" color="green">指定设备</Tag>
|
|
||||||
<Tag v-else>{{ record.deviceScope }}</Tag>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 升级进度 -->
|
|
||||||
<template v-else-if="column.key === 'progress'">
|
|
||||||
{{ record.deviceSuccessCount }}/{{ record.deviceTotalCount }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 任务状态 -->
|
|
||||||
<template v-else-if="column.key === 'status'">
|
|
||||||
<Tag v-if="record.status === 0" color="orange">待执行</Tag>
|
|
||||||
<Tag v-else-if="record.status === 1" color="blue">执行中</Tag>
|
|
||||||
<Tag v-else-if="record.status === 2" color="green">已完成</Tag>
|
|
||||||
<Tag v-else-if="record.status === 3" color="red">已取消</Tag>
|
|
||||||
<Tag v-else>{{ record.status }}</Tag>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 操作 -->
|
|
||||||
<template v-else-if="column.key === 'action'">
|
|
||||||
<Space>
|
|
||||||
<a @click="handleTaskDetail(record.id)">详情</a>
|
|
||||||
<a
|
|
||||||
v-if="record.status === IoTOtaTaskStatusEnum.IN_PROGRESS.value"
|
|
||||||
class="text-red-500"
|
|
||||||
@click="handleCancelTask(record.id)"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</a>
|
|
||||||
</Space>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<!-- 新增任务弹窗 -->
|
|
||||||
<OtaTaskForm
|
|
||||||
ref="taskFormRef"
|
|
||||||
:firmware-id="firmwareId"
|
|
||||||
:product-id="productId"
|
|
||||||
@success="handleTaskCreateSuccess"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 任务详情弹窗 -->
|
|
||||||
<OtaTaskDetail ref="taskDetailRef" @success="refresh" />
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Card, Col, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** OTA 升级设备统计卡片 */
|
||||||
|
defineOptions({ name: 'IoTOtaUpgradeStatistics' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
loading?: boolean;
|
||||||
|
statistics: Record<string, number>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 统计项配置 */
|
||||||
|
const items = computed(() => [
|
||||||
|
{
|
||||||
|
label: '升级设备总数',
|
||||||
|
span: 6,
|
||||||
|
color: 'text-blue-500',
|
||||||
|
value: Object.values(props.statistics).reduce(
|
||||||
|
(sum, count) => sum + (count || 0),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '待推送',
|
||||||
|
span: 3,
|
||||||
|
color: 'text-gray-400',
|
||||||
|
value: props.statistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已推送',
|
||||||
|
span: 3,
|
||||||
|
color: 'text-blue-400',
|
||||||
|
value: props.statistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '正在升级',
|
||||||
|
span: 3,
|
||||||
|
color: 'text-yellow-500',
|
||||||
|
value: props.statistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '升级成功',
|
||||||
|
span: 3,
|
||||||
|
color: 'text-green-500',
|
||||||
|
value: props.statistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '升级失败',
|
||||||
|
span: 3,
|
||||||
|
color: 'text-red-500',
|
||||||
|
value: props.statistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '升级取消',
|
||||||
|
span: 3,
|
||||||
|
color: 'text-gray-400',
|
||||||
|
value: props.statistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card title="升级设备统计" class="mb-5" :loading="loading">
|
||||||
|
<Row :gutter="20" class="py-5">
|
||||||
|
<Col v-for="item in items" :key="item.label" :span="item.span">
|
||||||
|
<div
|
||||||
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold" :class="item.color">
|
||||||
|
{{ item.value }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue