feat(iot): 优化 data rule,使用 vxe 简化表单,提升 antd、ele 的代码复用性

pull/345/head
YunaiV 2026-05-20 09:54:33 +08:00
parent b9d333f7ec
commit 538d04a380
2 changed files with 178 additions and 159 deletions

View File

@ -98,6 +98,42 @@ export function useRuleFormSchema(): VbenFormSchema[] {
];
}
/** 数据源配置(行编辑表)的字段 */
export function useSourceConfigColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'productId',
title: '产品',
minWidth: 200,
slots: { default: 'productId' },
},
{
field: 'deviceId',
title: '设备',
minWidth: 200,
slots: { default: 'deviceId' },
},
{
field: 'method',
title: '消息',
minWidth: 200,
slots: { default: 'method' },
},
{
field: 'identifier',
title: '标识符',
minWidth: 250,
slots: { default: 'identifier' },
},
{
title: '操作',
width: 80,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<DataRuleApi.DataRule>['columns'] {
return [

View File

@ -1,33 +1,30 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { computed, nextTick, onMounted, ref } from 'vue';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { Button, Form, Select, Table } from 'ant-design-vue';
import { Button, message, Select } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
import { useSourceConfigColumns } from '../data';
const formData = ref<any[]>([]);
const productList = ref<any[]>([]); //
const deviceList = ref<any[]>([]); //
const thingModelCache = ref<Map<number, any[]>>(new Map()); // key productId
const formRules: any = reactive({
productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
deviceId: [{ required: true, message: '设备不能为空', trigger: 'change' }],
method: [{ required: true, message: '消息方法不能为空', trigger: 'change' }],
});
const formRef = ref(); // Ref
/** 上行消息方法列表 */
const upstreamMethods = computed(() => {
return Object.values(IotDeviceMessageMethodEnum).filter(
(item) => item.upstream,
);
}); //
});
/** 根据产品 ID 过滤设备 */
function getFilteredDevices(productId: number) {
@ -87,7 +84,6 @@ async function loadDeviceList() {
/** 加载物模型数据 */
async function loadThingModel(productId: number) {
//
if (thingModelCache.value.has(productId)) {
return;
}
@ -99,19 +95,19 @@ async function loadThingModel(productId: number) {
}
}
/** 产品变化时处理 */
async function handleProductChange(row: any, _index: number) {
/** 产品变化时清空设备 / 消息 / 标识符 */
function handleProductChange(rowIndex: number) {
const row = formData.value[rowIndex];
row.deviceId = 0;
row.method = undefined;
row.identifier = undefined;
row.identifierLoading = false;
}
/** 消息方法变化时处理 */
async function handleMethodChange(row: any, _index: number) {
//
/** 消息方法变化时清空标识符 + 按需加载物模型 */
async function handleMethodChange(rowIndex: number) {
const row = formData.value[rowIndex];
row.identifier = undefined;
//
if (shouldShowIdentifierSelect(row) && row.productId) {
row.identifierLoading = true;
await loadThingModel(row.productId);
@ -119,189 +115,176 @@ async function handleMethodChange(row: any, _index: number) {
}
}
/** 新增按钮操作 */
function handleAdd() {
const row = {
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useSourceConfigColumns(),
data: formData.value,
minHeight: 160,
border: true,
showOverflow: false,
rowConfig: { isHover: true, height: 64 },
pagerConfig: { enabled: false },
toolbarConfig: { enabled: false },
},
});
/** 同步 formData 到 vxe-grid */
async function reloadGrid() {
await nextTick();
await gridApi.grid?.reloadData(formData.value);
}
/** 新增一行数据源 */
async function handleAdd() {
formData.value.push({
productId: undefined,
deviceId: undefined,
method: undefined,
identifier: undefined,
identifierLoading: false,
};
formData.value.push(row);
});
await reloadGrid();
}
/** 删除按钮操作 */
function handleDelete(index: number) {
formData.value.splice(index, 1);
/** 删除一行数据源 */
async function handleDelete(rowIndex: number) {
formData.value.splice(rowIndex, 1);
await reloadGrid();
}
/** 表单校验 */
/** 校验全部行;返回 Promise失败时 reject 第一条错误信息 */
function validate() {
return formRef.value.validate();
for (let i = 0; i < formData.value.length; i++) {
const row = formData.value[i];
if (!row.productId) {
message.error(`${i + 1} 行:产品不能为空`);
return Promise.reject(new Error('产品不能为空'));
}
if (row.deviceId === undefined || row.deviceId === null) {
message.error(`${i + 1} 行:设备不能为空`);
return Promise.reject(new Error('设备不能为空'));
}
if (!row.method) {
message.error(`${i + 1} 行:消息方法不能为空`);
return Promise.reject(new Error('消息方法不能为空'));
}
}
return Promise.resolve();
}
/** 表单值 */
/** 取当前所有行的值 */
function getData() {
return formData.value;
}
/** 设置表单值 */
function setData(data: any[]) {
//
/** 设置初始数据 */
async function setData(data: any[]) {
formData.value = (data || []).map((item) => ({
...item,
identifierLoading: false,
}));
//
// TODO @AI linter Promise returned from forEach argument is ignored
data?.forEach(async (item) => {
if (item.productId && shouldShowIdentifierSelect(item)) {
await loadThingModel(item.productId);
}
});
await reloadGrid();
}
/** 初始化 */
onMounted(async () => {
await Promise.all([loadProductList(), loadDeviceList()]);
});
const columns = [
{
title: '产品',
dataIndex: 'productId',
width: 200,
},
{
title: '设备',
dataIndex: 'deviceId',
width: 200,
},
{
title: '消息',
dataIndex: 'method',
width: 200,
},
{
title: '标识符',
dataIndex: 'identifier',
width: 250,
},
{
title: '操作',
width: 80,
fixed: 'right' as const,
},
];
defineExpose({ validate, getData, setData });
</script>
<template>
<Form ref="formRef" :model="{ data: formData }">
<Table
:columns="columns"
:data-source="formData"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'productId'">
<Form.Item
:name="['data', index, 'productId']"
:rules="formRules.productId"
class="mb-0"
>
<Select
v-model:value="record.productId"
placeholder="请选择产品"
show-search
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="
productList.map((p: any) => ({ label: p.name, value: p.id }))
"
@change="() => handleProductChange(record, index)"
/>
</Form.Item>
</template>
<template v-else-if="column.dataIndex === 'deviceId'">
<Form.Item
:name="['data', index, 'deviceId']"
:rules="formRules.deviceId"
class="mb-0"
>
<Select
v-model:value="record.deviceId"
placeholder="请选择设备"
show-search
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="[
{ label: '全部设备', value: 0 },
...getFilteredDevices(record.productId).map((d: any) => ({
label: d.deviceName,
value: d.id,
})),
]"
/>
</Form.Item>
</template>
<template v-else-if="column.dataIndex === 'method'">
<Form.Item
:name="['data', index, 'method']"
:rules="formRules.method"
class="mb-0"
>
<Select
v-model:value="record.method"
placeholder="请选择消息"
show-search
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="
upstreamMethods.map((m: any) => ({
label: m.name,
value: m.method,
}))
"
@change="() => handleMethodChange(record, index)"
/>
</Form.Item>
</template>
<template v-else-if="column.dataIndex === 'identifier'">
<Form.Item :name="['data', index, 'identifier']" class="mb-0">
<Select
v-if="shouldShowIdentifierSelect(record)"
v-model:value="record.identifier"
placeholder="请选择标识符"
show-search
:loading="record.identifierLoading"
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="getThingModelOptions(record)"
/>
</Form.Item>
</template>
<template v-else-if="column.title === ''">
<Button type="link" danger @click="handleDelete(index)"></Button>
</template>
<div>
<Grid>
<template #productId="{ rowIndex }">
<Select
v-model:value="formData[rowIndex].productId"
placeholder="请选择产品"
show-search
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="
productList.map((p: any) => ({ label: p.name, value: p.id }))
"
class="w-full"
@change="() => handleProductChange(rowIndex)"
/>
</template>
</Table>
<template #deviceId="{ rowIndex }">
<Select
v-model:value="formData[rowIndex].deviceId"
placeholder="请选择设备"
show-search
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="[
{ label: '全部设备', value: 0 },
...getFilteredDevices(formData[rowIndex].productId).map(
(d: any) => ({
label: d.deviceName,
value: d.id,
}),
),
]"
class="w-full"
/>
</template>
<template #method="{ rowIndex }">
<Select
v-model:value="formData[rowIndex].method"
placeholder="请选择消息"
show-search
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="
upstreamMethods.map((m: any) => ({
label: m.name,
value: m.method,
}))
"
class="w-full"
@change="() => handleMethodChange(rowIndex)"
/>
</template>
<template #identifier="{ rowIndex }">
<Select
v-if="shouldShowIdentifierSelect(formData[rowIndex])"
v-model:value="formData[rowIndex].identifier"
placeholder="请选择标识符"
show-search
:loading="formData[rowIndex].identifierLoading"
:filter-option="
(input: string, option: any) =>
option.label.toLowerCase().includes(input.toLowerCase())
"
:options="getThingModelOptions(formData[rowIndex])"
class="w-full"
/>
<span v-else class="text-xs text-secondary">-</span>
</template>
<template #actions="{ rowIndex }">
<Button danger type="link" @click="handleDelete(rowIndex)"></Button>
</template>
</Grid>
<div class="mt-3 text-center">
<Button type="primary" @click="handleAdd">
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
添加数据源
</Button>
</div>
</Form>
</div>
</template>