feat(iot): 优化 data rule,使用 vxe 简化表单,提升 antd、ele 的代码复用性
parent
b9d333f7ec
commit
538d04a380
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue