feat(iot):优化 ota 的代码风格

pull/345/head
YunaiV 2026-05-18 21:14:20 +08:00
parent 76bed17ed9
commit 584370358e
16 changed files with 849 additions and 1552 deletions

View File

@ -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' },
},
];
}

View File

@ -129,11 +129,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '固件描述',
minWidth: 200,
},
// TODO @AI你看看 vue3 + ep 是这么干的么?
{
field: 'productId',
field: 'productName',
title: '所属产品',
minWidth: 150,
slots: { default: 'product' },
formatter: ({ row }) => row.productName || '未知产品',
},
{
field: 'fileUrl',

View File

@ -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>

View File

@ -13,11 +13,9 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { $t } from '#/locales';
import OtaFirmwareForm from '../modules/ota-firmware-form.vue';
import OtaFirmwareForm from '../modules/firmware-form.vue';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'IoTOtaFirmware' });
const { push } = useRouter();
const [FormModal, formModalApi] = useVbenModal({
@ -104,19 +102,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('ui.actionTitle.create', ['固件']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['iot:ota-firmware:create'],
onClick: handleCreate,
},
]"
/>
</template>
<!-- 产品名称列 -->
<template #product="{ row }">
<span class="text-gray-700">{{ row.productName || '未知产品' }}</span>
</template>
<!-- 固件文件列 -->
<template #fileUrl="{ row }">
<!-- TODO @AI使用 TableAction 按钮 -->
<div
v-if="row.fileUrl"
class="inline-flex items-center gap-1.5 align-middle leading-none"
@ -136,8 +130,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
</div>
<span v-else class="text-gray-400">无文件</span>
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<TableAction
:actions="[
@ -145,12 +137,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['iot:ota-firmware:query'],
onClick: handleDetail.bind(null, row),
},
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['iot:ota-firmware:update'],
onClick: handleEdit.bind(null, row),
},
{
@ -158,6 +152,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
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),

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
// TODO @AI firmware/modules/form.vue
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import { computed, ref } from 'vue';

View File

@ -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' },
},
];
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>