feat(iot):优化 ota 的代码风格(v3)
parent
a70fcc9616
commit
9d54f60b10
|
|
@ -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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 @AI:vben 用 row.productName(后端 VO 已返回);vue3 + ep 用 getProductName(productId) 本地查表;vben 更直接,且 IoTOtaFirmwareApi.Firmware.productName 已是 API 契约
|
||||
// TODO @AI:实际后端没读取哈。看看怎么对齐下 vue3 + ep
|
||||
// TODO DONE @AI:后端 firmware 没返回 productName,formatter 调 getProductName resolver;resolver + productList 反应式状态由 index.vue 注入(对齐 infra/codegen 模式)
|
||||
{
|
||||
field: 'productName',
|
||||
field: 'productId',
|
||||
title: '所属产品',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => row.productName || '未知产品',
|
||||
formatter: ({ cellValue }) =>
|
||||
getProductName?.(cellValue) || (cellValue ? '加载中...' : '-'),
|
||||
},
|
||||
{
|
||||
field: 'fileUrl',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 @AI:不需要;script setup 默认用文件名生成 __name(这里是 info),devtools 能识别;跟之前 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 @AI:record schema 已挪到 task/record/data.ts;list 也独立成 task/record/modules/list.vue
|
||||
|
|
|
|||
|
|
@ -1,31 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
// TODO @AI:是不是拆分下,task 里面在新建一个 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 @AI:是不是defineOptions、升级任务详情 注释需要?
|
||||
/** 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>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { $t } from '#/locales';
|
|||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
// TODO @AI:是不是defineOptions、升级任务表单 注释需要?
|
||||
/** 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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 @AI:defineOptions 还需要么?
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 @AI:label 名字,是不是通过字典获取名字?包括 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
|
||||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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 @AI:defineOptions({ 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>
|
||||
Loading…
Reference in New Issue