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

pull/345/head
YunaiV 2026-05-19 14:29:14 +08:00
parent a70fcc9616
commit 9d54f60b10
13 changed files with 416 additions and 282 deletions

View File

@ -37,7 +37,7 @@ const routes: RouteRecordRaw[] = [
activePath: '/iot/ota',
},
component: () =>
import('#/views/iot/ota/modules/firmware-detail/index.vue'),
import('#/views/iot/ota/firmware/detail/index.vue'),
},
],
},

View File

@ -124,7 +124,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(
getProductName?: (productId: number) => string | undefined,
): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
@ -147,13 +149,13 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '固件描述',
minWidth: 200,
},
// TODO DONE @AIvben 用 row.productName后端 VO 已返回vue3 + ep 用 getProductName(productId) 本地查表vben 更直接,且 IoTOtaFirmwareApi.Firmware.productName 已是 API 契约
// TODO @AI实际后端没读取哈。看看怎么对齐下 vue3 + ep
// TODO DONE @AI后端 firmware 没返回 productNameformatter 调 getProductName resolverresolver + productList 反应式状态由 index.vue 注入(对齐 infra/codegen 模式)
{
field: 'productName',
field: 'productId',
title: '所属产品',
minWidth: 150,
formatter: ({ row }) => row.productName || '未知产品',
formatter: ({ cellValue }) =>
getProductName?.(cellValue) || (cellValue ? '加载中...' : '-'),
},
{
field: 'fileUrl',

View File

@ -4,11 +4,13 @@ import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { Page } from '@vben/common-ui';
import { getOtaFirmware } from '#/api/iot/ota/firmware';
import { getOtaTaskRecordStatusStatistics } from '#/api/iot/ota/task/record';
import OtaTaskList from '../../task/modules/list.vue';
import UpgradeStatistics from '../../task/modules/upgrade-statistics.vue';
import UpgradeStatistics from '../../task/modules/statistics.vue';
import FirmwareInfo from './modules/info.vue';
const route = useRoute();
@ -50,23 +52,23 @@ onMounted(() => {
</script>
<template>
<div class="p-4">
<Page>
<!-- 固件信息 -->
<FirmwareInfo :firmware="firmware" :loading="firmwareLoading" />
<!-- 升级设备统计 -->
<!-- TODO @AI需要和上面的 FirmwareInfo 有间隙 -->
<UpgradeStatistics
:loading="firmwareStatisticsLoading"
:statistics="firmwareStatistics"
/>
<div class="mt-4">
<UpgradeStatistics
:loading="firmwareStatisticsLoading"
:statistics="firmwareStatistics"
/>
</div>
<!-- 任务管理 -->
<OtaTaskList
v-if="firmware?.productId"
:firmware-id="firmwareId"
:product-id="firmware.productId"
@success="getStatistics"
/>
</div>
<div v-if="firmware?.productId" class="mt-4">
<OtaTaskList
:firmware-id="firmwareId"
:product-id="firmware.productId"
@success="getStatistics"
/>
</div>
</Page>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import { Card } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../../data';
/** IoT OTA 固件基本信息 */
// TODO DONE @AIscript setup __name infodevtools upgrade-statistics / list
defineProps<{
firmware: IoTOtaFirmwareApi.Firmware;
loading?: boolean;
}>();
const [Description] = useDescription({
bordered: true,
column: 3,
schema: useDetailSchema(),
});
</script>
<template>
<Card title="固件信息" :loading="loading">
<Description :data="firmware" />
</Card>
</template>

View File

@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
import type { IotProductApi } from '#/api/iot/product/product';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
@ -11,6 +13,7 @@ import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOtaFirmware, getOtaFirmwarePage } from '#/api/iot/ota/firmware';
import { getSimpleProductList } from '#/api/iot/product/product';
import { $t } from '#/locales';
import OtaFirmwareForm from './modules/form.vue';
@ -18,6 +21,18 @@ import { useGridColumns, useGridFormSchema } from './data';
const { push } = useRouter();
const productList = ref<IotProductApi.Product[]>([]);
/** 根据产品编号查找名称 */
// TODO @AI data.ts
function getProductName(productId: number | undefined) {
if (!productId) {
return '-';
}
const product = productList.value.find((p) => p.id === productId);
return product ? product.name : '加载中...';
}
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: OtaFirmwareForm,
destroyOnClose: true,
@ -65,7 +80,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
columns: useGridColumns(getProductName),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -89,6 +104,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
} as VxeTableGridOptions<IoTOtaFirmwareApi.Firmware>,
});
/** 初始化加载产品列表 */
onMounted(async () => {
productList.value = (await getSimpleProductList()) || [];
});
</script>
<template>

View File

@ -1,11 +1,37 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getDictLabel, getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
/** 任务详情的描述字段 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{ field: 'id', label: '任务编号' },
{ field: 'name', label: '任务名称' },
{
field: 'deviceScope',
label: '升级范围',
render: (val) => getDictLabel(DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE, val),
},
{
field: 'status',
label: '任务状态',
render: (val) => getDictLabel(DICT_TYPE.IOT_OTA_TASK_STATUS, val),
},
{
field: 'createTime',
label: '创建时间',
render: (val) => (val ? (formatDateTime(val) as string) : '-'),
},
{ field: 'description', label: '任务描述', span: 3 },
];
}
/** 新增升级任务的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -71,21 +97,7 @@ export function useFormSchema(): VbenFormSchema[] {
];
}
/** 任务列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '任务名称',
component: 'Input',
componentProps: {
placeholder: '请输入任务名称',
allowClear: true,
},
},
];
}
// TODO DONE @AI任务列表内嵌固件详情页单字段搜索意义不大已去掉搜索表单
/** 任务列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
@ -153,58 +165,4 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
];
}
/** 升级记录的列表字段 */
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' },
},
];
}
// TODO DONE @AIrecord schema 已挪到 task/record/data.tslist 也独立成 task/record/modules/list.vue

View File

@ -1,31 +1,21 @@
<script setup lang="ts">
// TODO @AItask record/modules
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 { 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 { useRecordGridColumns } from '../data';
import UpgradeStatistics from './upgrade-statistics.vue';
import OtaTaskRecordList from '../record/modules/list.vue';
import TaskInfo from './info.vue';
import UpgradeStatistics from './statistics.vue';
/** OTA 任务详情组件 */
// TODO @AIdefineOptions
/** IoT OTA 升级任务详情 */
defineOptions({ name: 'IoTOtaTaskDetail' });
const emit = defineEmits(['success']);
@ -37,20 +27,6 @@ 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) {
@ -80,54 +56,13 @@ async function getStatistics() {
}
}
/** 切换标签 */
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();
/** 单条记录取消后,刷新任务信息和统计 */
async function handleRecordCancelled() {
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) {
@ -138,94 +73,30 @@ const [Modal, modalApi] = useVbenModal({
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">
<!-- TODO @AI是不是不用 p-4 外面的 -->
<div class="p-4">
<!-- 任务信息 -->
<!-- TODO @AI需要抽成一个小 vue 组件也使用 description 组件 -->
<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>
<!-- 任务升级设备统计 -->
<!-- TODO @AI和上面有间隙 -->
<Modal
title="升级任务详情"
class="w-5/6"
:show-cancel-button="false"
:show-confirm-button="false"
>
<!-- 任务信息 -->
<TaskInfo :task="task" :loading="taskLoading" />
<!-- 升级设备统计 -->
<div class="mt-4">
<UpgradeStatistics
:loading="taskStatisticsLoading"
:statistics="taskStatistics"
/>
<!-- 设备管理 -->
<!-- TODO @AI需要抽成一个小 vue 组件 -->
<!-- TODO @AI和上面有间隙 -->
<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>
<!-- 升级设备记录 -->
<div class="mt-4">
<OtaTaskRecordList :task-id="taskId" @cancelled="handleRecordCancelled" />
</div>
</Modal>
</template>

View File

@ -12,6 +12,7 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
// TODO @AIdefineOptions
/** IoT OTA 升级任务表单 */
defineOptions({ name: 'IoTOtaTaskForm' });
@ -28,6 +29,7 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// TODO @AI form
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
import { Card } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
// TODO @AI
/** IoT OTA 升级任务基本信息 */
defineOptions({ name: 'IoTOtaTaskInfo' });
defineProps<{
loading?: boolean;
task: IoTOtaTaskApi.Task;
}>();
const [Description] = useDescription({
bordered: true,
column: 3,
schema: useDetailSchema(),
});
</script>
<template>
<Card title="任务信息" :loading="loading">
<Description :data="task" />
</Card>
</template>

View File

@ -2,21 +2,19 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { Input, 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 { useGridColumns } from '../data';
import OtaTaskDetail from './detail.vue';
import OtaTaskForm from './form.vue';
import { useGridColumns, useGridFormSchema } from '../data';
/** IoT OTA 任务列表 */
// TODO @AIdefineOptions
defineOptions({ name: 'IoTOtaTaskList' });
const props = defineProps<{
firmwareId: number;
@ -25,6 +23,8 @@ const props = defineProps<{
const emit = defineEmits(['success']);
const searchName = ref('');
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: OtaTaskForm,
destroyOnClose: true,
@ -41,6 +41,11 @@ async function handleRefresh() {
emit('success');
}
/** 按任务名搜索(嵌入页面里,单字段搜索做成 toolbar 内联输入框,回车 / 清空触发查询) */
async function handleSearch() {
await gridApi.query();
}
/** 新增任务 */
function handleCreate() {
formModalApi
@ -61,21 +66,18 @@ async function handleCancel(row: IoTOtaTaskApi.Task) {
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
maxHeight: 500,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
query: async ({ page }) => {
return await getOtaTaskPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
firmwareId: props.firmwareId,
...formValues,
name: searchName.value || undefined,
});
},
},
@ -86,31 +88,44 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<IoTOtaTaskApi.Task>,
});
</script>
<template>
<!-- TODO @AI是不是要有个高度 -->
<div>
<FormModal @success="handleRefresh" />
<DetailModal @success="handleRefresh" />
<!-- TODO @AI上面有个任务名称有没可能包在一起不然太丑了/Users/yunai/Downloads/iShot_2026-05-19_11.26.04.png -->
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
<div class="flex items-center gap-2">
<Input
v-model:value="searchName"
placeholder="请输入任务名称"
allow-clear
style="width: 200px"
@press-enter="handleSearch"
@change="(e: any) => !e.target.value && handleSearch()"
/>
<TableAction
:actions="[
{
label: '搜索',
type: 'default',
icon: 'ant-design:search-outlined',
onClick: handleSearch,
},
{
label: '新增',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</div>
</template>
<template #actions="{ row }">
<TableAction

View File

@ -1,19 +1,24 @@
<script setup lang="ts">
// TODO @AI statistics.vue
import { computed } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { Card, Col, Row } from 'ant-design-vue';
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
/** OTA 升级设备统计卡片 */
// TODO @AI defineOptions
defineOptions({ name: 'IoTOtaUpgradeStatistics' });
const props = defineProps<{
loading?: boolean;
statistics: Record<string, number>;
}>();
/** 取字典标签(同步) */
function dictLabel(value: number) {
return getDictLabel(DICT_TYPE.IOT_OTA_TASK_RECORD_STATUS, value);
}
/** 统计项配置 */
const items = computed(() => [
{
@ -25,39 +30,38 @@ const items = computed(() => [
0,
),
},
// TODO @AIlabel vue3 + ep
{
label: '待推送',
label: dictLabel(IoTOtaTaskRecordStatusEnum.PENDING.value),
span: 3,
color: 'text-gray-400',
value: props.statistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0,
},
{
label: '已推送',
label: dictLabel(IoTOtaTaskRecordStatusEnum.PUSHED.value),
span: 3,
color: 'text-blue-400',
value: props.statistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0,
},
{
label: '正在升级',
label: dictLabel(IoTOtaTaskRecordStatusEnum.UPGRADING.value),
span: 3,
color: 'text-yellow-500',
value: props.statistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0,
},
{
label: '升级成功',
label: dictLabel(IoTOtaTaskRecordStatusEnum.SUCCESS.value),
span: 3,
color: 'text-green-500',
value: props.statistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0,
},
{
label: '升级失败',
label: dictLabel(IoTOtaTaskRecordStatusEnum.FAILURE.value),
span: 3,
color: 'text-red-500',
value: props.statistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0,
},
{
label: '升级取消',
label: dictLabel(IoTOtaTaskRecordStatusEnum.CANCELED.value),
span: 3,
color: 'text-gray-400',
value: props.statistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0,
@ -66,7 +70,7 @@ const items = computed(() => [
</script>
<template>
<Card title="升级设备统计" class="mb-5" :loading="loading">
<Card title="升级设备统计" :loading="loading">
<Row :gutter="20" class="py-5">
<Col v-for="item in items" :key="item.label" :span="item.span">
<div

View File

@ -0,0 +1,59 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
/** 升级记录的列表字段 */
export function useGridColumns(): 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,143 @@
<script setup lang="ts">
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IoTOtaTaskRecordApi } from '#/api/iot/ota/task/record';
import { computed, ref, watch } from 'vue';
import { Card, message, Tabs } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelOtaTaskRecord,
getOtaTaskRecordPage,
} from '#/api/iot/ota/task/record';
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
import { useGridColumns } from '../data';
/** IoT OTA 升级记录列表 */
// TODO @AIdefineOptions({ name: 'IoTOtaTaskRecordList' });
defineOptions({ name: 'IoTOtaTaskRecordList' });
const props = defineProps<{
taskId: number | undefined;
}>();
const emit = defineEmits<{
cancelled: [];
}>();
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 handleTabChange(tabKey: number | string) {
activeTab.value = String(tabKey);
await gridApi.query();
}
/** 取消单条记录的升级 */
// TODO @AI
async function handleCancelUpgrade(record: IoTOtaTaskRecordApi.TaskRecord) {
await cancelOtaTaskRecord(record.id as number);
message.success('取消成功');
await gridApi.query();
emit('cancelled');
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 400,
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.taskId) {
return { list: [], total: 0 };
}
return await getOtaTaskRecordPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
taskId: props.taskId,
// TODO @AI formValues
status: activeTab.value === '' ? undefined : Number(activeTab.value),
});
},
},
},
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<IoTOtaTaskRecordApi.TaskRecord>,
});
/** taskId 变化时重新查询 */
watch(
() => props.taskId,
(val) => {
if (val) {
activeTab.value = '';
gridApi.query();
}
},
);
defineExpose({ refresh: () => gridApi.query() });
</script>
<template>
<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>
</template>