Vue3 + Element Plus版本iot前端迁移到vben版本
This commit is contained in:
170
apps/web-antd/src/views/iot/ota/data.ts
Normal file
170
apps/web-antd/src/views/iot/ota/data.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
207
apps/web-antd/src/views/iot/ota/firmware/data.ts
Normal file
207
apps/web-antd/src/views/iot/ota/firmware/data.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
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: 'FileUpload',
|
||||
componentProps: {
|
||||
maxNumber: 1,
|
||||
accept: ['bin', 'hex', 'zip'],
|
||||
maxSize: 50,
|
||||
helpText: '支持上传 .bin、.hex、.zip 格式的固件文件,最大 50MB',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
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(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '固件编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '固件名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: '版本号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '固件描述',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'productId',
|
||||
title: '所属产品',
|
||||
minWidth: 150,
|
||||
slots: { default: 'product' },
|
||||
},
|
||||
{
|
||||
field: 'fileUrl',
|
||||
title: '固件文件',
|
||||
minWidth: 120,
|
||||
slots: { default: 'fileUrl' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Grid 配置项 */
|
||||
export function useGridOptions(): VxeTableGridOptions<IoTOtaFirmwareApi.Firmware> {
|
||||
return {
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 删除固件 */
|
||||
export async function handleDeleteFirmware(
|
||||
row: IoTOtaFirmwareApi.Firmware,
|
||||
onSuccess: () => void,
|
||||
) {
|
||||
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]),
|
||||
});
|
||||
onSuccess();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
124
apps/web-antd/src/views/iot/ota/firmware/index.vue
Normal file
124
apps/web-antd/src/views/iot/ota/firmware/index.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import {
|
||||
handleDeleteFirmware,
|
||||
useGridFormSchema,
|
||||
useGridOptions,
|
||||
} from './data';
|
||||
import Form from '../modules/OtaFirmwareForm.vue';
|
||||
|
||||
defineOptions({ name: 'IoTOtaFirmware' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建固件 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ type: 'create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑固件 */
|
||||
function handleEdit(row: IoTOtaFirmwareApi.Firmware) {
|
||||
formModalApi.setData({ type: 'update', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除固件 */
|
||||
async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
|
||||
await handleDeleteFirmware(row, onRefresh);
|
||||
}
|
||||
|
||||
/** 查看固件详情 */
|
||||
function handleDetail(row: IoTOtaFirmwareApi.Firmware) {
|
||||
formModalApi.setData({ type: 'view', id: row.id }).open();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: useGridOptions(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="固件列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['固件']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 产品名称列 -->
|
||||
<template #product="{ row }">
|
||||
<span class="text-gray-700">{{ row.productName || '未知产品' }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 固件文件列 -->
|
||||
<template #fileUrl="{ row }">
|
||||
<a
|
||||
v-if="row.fileUrl"
|
||||
:href="row.fileUrl"
|
||||
target="_blank"
|
||||
download
|
||||
class="text-primary cursor-pointer hover:underline"
|
||||
>
|
||||
<Icon icon="ant-design:download-outlined" class="mr-1" />
|
||||
下载固件
|
||||
</a>
|
||||
<span v-else class="text-gray-400">无文件</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
129
apps/web-antd/src/views/iot/ota/index.vue
Normal file
129
apps/web-antd/src/views/iot/ota/index.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<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/OtaFirmwareForm.vue';
|
||||
|
||||
defineOptions({ name: 'IoTOtaFirmware' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: OtaFirmwareForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
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]),
|
||||
});
|
||||
onRefresh();
|
||||
} 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="onRefresh" />
|
||||
<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>
|
||||
90
apps/web-antd/src/views/iot/ota/modules/OtaFirmwareForm.vue
Normal file
90
apps/web-antd/src/views/iot/ota/modules/OtaFirmwareForm.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createOtaFirmware,
|
||||
getOtaFirmware,
|
||||
updateOtaFirmware,
|
||||
} from '#/api/iot/ota/firmware';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../firmware/data';
|
||||
|
||||
defineOptions({ name: 'IoTOtaFirmwareForm' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const formData = ref<IoTOtaFirmwareApi.Firmware>();
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['固件'])
|
||||
: $t('ui.actionTitle.create', ['固件']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useFormSchema(),
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as IoTOtaFirmwareApi.Firmware;
|
||||
try {
|
||||
await (formData.value?.id ? updateOtaFirmware(data) : createOtaFirmware(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IoTOtaFirmwareApi.Firmware>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getOtaFirmware(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/3">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
149
apps/web-antd/src/views/iot/ota/modules/detail/index.vue
Normal file
149
apps/web-antd/src/views/iot/ota/modules/detail/index.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
||||
import * as IoTOtaFirmwareApi from '#/api/iot/ota/firmware';
|
||||
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
import OtaTaskList from '../task/OtaTaskList.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>>({});
|
||||
|
||||
/** 获取固件信息 */
|
||||
const getFirmwareInfo = async () => {
|
||||
firmwareLoading.value = true;
|
||||
try {
|
||||
firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value);
|
||||
} finally {
|
||||
firmwareLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取升级统计 */
|
||||
const getStatistics = async () => {
|
||||
firmwareStatisticsLoading.value = true;
|
||||
try {
|
||||
firmwareStatistics.value = await IoTOtaTaskRecordApi.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="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 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="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">待推送</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-blue-400">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">已推送</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-yellow-500">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">正在升级</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-green-500">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">升级成功</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-red-500">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">升级失败</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 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>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
||||
import * as IoTOtaFirmwareApi from '#/api/iot/ota/firmware';
|
||||
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
import OtaTaskList from '../task/OtaTaskList.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>>({});
|
||||
|
||||
/** 获取固件信息 */
|
||||
const getFirmwareInfo = async () => {
|
||||
firmwareLoading.value = true;
|
||||
try {
|
||||
firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value);
|
||||
} finally {
|
||||
firmwareLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取升级统计 */
|
||||
const getStatistics = async () => {
|
||||
firmwareStatisticsLoading.value = true;
|
||||
try {
|
||||
firmwareStatistics.value = await IoTOtaTaskRecordApi.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="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 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="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">待推送</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-blue-400">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">已推送</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-yellow-500">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">正在升级</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-green-500">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">升级成功</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-red-500">
|
||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">升级失败</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 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>
|
||||
349
apps/web-antd/src/views/iot/ota/modules/task/OtaTaskDetail.vue
Normal file
349
apps/web-antd/src/views/iot/ota/modules/task/OtaTaskDetail.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Card, Col, Descriptions, Modal, Row, Table, Tabs, Tag, message } from 'ant-design-vue';
|
||||
import type { TableColumnsType } from 'ant-design-vue';
|
||||
import type { OtaTask } from '#/api/iot/ota/task';
|
||||
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
||||
import type { OtaTaskRecord } from '#/api/iot/ota/task/record';
|
||||
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
/** 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();
|
||||
|
||||
/** 获取任务详情 */
|
||||
const getTaskInfo = async () => {
|
||||
if (!taskId.value) {
|
||||
return;
|
||||
}
|
||||
taskLoading.value = true;
|
||||
try {
|
||||
task.value = await IoTOtaTaskApi.getOtaTask(taskId.value);
|
||||
} finally {
|
||||
taskLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取统计数据 */
|
||||
const getStatistics = async () => {
|
||||
if (!taskId.value) {
|
||||
return;
|
||||
}
|
||||
taskStatisticsLoading.value = true;
|
||||
try {
|
||||
taskStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
||||
undefined,
|
||||
taskId.value,
|
||||
);
|
||||
} finally {
|
||||
taskStatisticsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取升级记录列表 */
|
||||
const getRecordList = async () => {
|
||||
if (!taskId.value) {
|
||||
return;
|
||||
}
|
||||
recordLoading.value = true;
|
||||
try {
|
||||
queryParams.taskId = taskId.value;
|
||||
const data = await IoTOtaTaskRecordApi.getOtaTaskRecordPage(queryParams);
|
||||
recordList.value = data.list || [];
|
||||
recordTotal.value = data.total || 0;
|
||||
} finally {
|
||||
recordLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 切换标签 */
|
||||
const handleTabChange = (tabKey: string | number) => {
|
||||
activeTab.value = String(tabKey);
|
||||
queryParams.pageNo = 1;
|
||||
queryParams.status = activeTab.value === '' ? undefined : parseInt(String(tabKey));
|
||||
getRecordList();
|
||||
};
|
||||
|
||||
/** 分页变化 */
|
||||
const handleTableChange = (pagination: any) => {
|
||||
queryParams.pageNo = pagination.current;
|
||||
queryParams.pageSize = pagination.pageSize;
|
||||
getRecordList();
|
||||
};
|
||||
|
||||
/** 取消升级 */
|
||||
const handleCancelUpgrade = async (record: OtaTaskRecord) => {
|
||||
Modal.confirm({
|
||||
title: '确认取消',
|
||||
content: '确认要取消该设备的升级任务吗?',
|
||||
async onOk() {
|
||||
try {
|
||||
await IoTOtaTaskRecordApi.cancelOtaTaskRecord(record.id!);
|
||||
message.success('取消成功');
|
||||
await getRecordList();
|
||||
await getStatistics();
|
||||
await getTaskInfo();
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
console.error('取消升级失败', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** 打开弹窗 */
|
||||
const 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="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 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="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">待推送</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-blue-400">
|
||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">已推送</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-yellow-500">
|
||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">正在升级</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-green-500">
|
||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">升级成功</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 text-red-500">
|
||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">升级失败</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
||||
<div class="text-3xl font-bold mb-2 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:activeKey="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>
|
||||
148
apps/web-antd/src/views/iot/ota/modules/task/OtaTaskForm.vue
Normal file
148
apps/web-antd/src/views/iot/ota/modules/task/OtaTaskForm.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { message, Form, Input, Select, Spin } from 'ant-design-vue';
|
||||
import type { OtaTask } from '#/api/iot/ota/task';
|
||||
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
||||
import type { DeviceVO } from '#/api/iot/device/device';
|
||||
import * as DeviceApi from '#/api/iot/device/device';
|
||||
|
||||
/** 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<DeviceVO[]>([]);
|
||||
|
||||
/** 设备选项 */
|
||||
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 IoTOtaTaskApi.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 DeviceApi.getDeviceListByProductId(props.productId)) || [];
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
name: '',
|
||||
deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
|
||||
firmwareId: props.firmwareId,
|
||||
description: '',
|
||||
deviceIds: [],
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async () => {
|
||||
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>
|
||||
242
apps/web-antd/src/views/iot/ota/modules/task/OtaTaskList.vue
Normal file
242
apps/web-antd/src/views/iot/ota/modules/task/OtaTaskList.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import type { OtaTask } from '#/api/iot/ota/task';
|
||||
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
||||
import OtaTaskForm from './OtaTaskForm.vue';
|
||||
import OtaTaskDetail from './OtaTaskDetail.vue';
|
||||
import { Card, Input, Table, Space, Modal, message, Tag } from 'ant-design-vue';
|
||||
import type { TableColumnsType } from 'ant-design-vue';
|
||||
import { VbenButton } from '@vben/common-ui';
|
||||
|
||||
/** 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(); // 任务详情引用
|
||||
|
||||
/** 获取任务列表 */
|
||||
const getTaskList = async () => {
|
||||
taskLoading.value = true;
|
||||
try {
|
||||
const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams);
|
||||
taskList.value = data.list;
|
||||
taskTotal.value = data.total;
|
||||
} finally {
|
||||
taskLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 搜索 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1;
|
||||
getTaskList();
|
||||
};
|
||||
|
||||
/** 打开任务表单 */
|
||||
const openTaskForm = () => {
|
||||
taskFormRef.value?.open();
|
||||
};
|
||||
|
||||
/** 处理任务创建成功 */
|
||||
const handleTaskCreateSuccess = () => {
|
||||
getTaskList();
|
||||
emit('success');
|
||||
};
|
||||
|
||||
/** 查看任务详情 */
|
||||
const handleTaskDetail = (id: number) => {
|
||||
taskDetailRef.value?.open(id);
|
||||
};
|
||||
|
||||
/** 取消任务 */
|
||||
const handleCancelTask = async (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认取消',
|
||||
content: '确认要取消该升级任务吗?',
|
||||
async onOk() {
|
||||
try {
|
||||
await IoTOtaTaskApi.cancelOtaTask(id);
|
||||
message.success('取消成功');
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
console.error('取消任务失败', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** 刷新数据 */
|
||||
const refresh = async () => {
|
||||
await getTaskList();
|
||||
emit('success');
|
||||
};
|
||||
|
||||
/** 分页变化 */
|
||||
const 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 justify-between items-center">
|
||||
<VbenButton type="primary" @click="openTaskForm">
|
||||
<Icon icon="ant-design:plus-outlined" class="mr-1" />
|
||||
新增
|
||||
</VbenButton>
|
||||
<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>
|
||||
Reference in New Issue
Block a user