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'] {
|
export function useGridColumns(): VxeTableGridOptions<DataRuleApi.DataRule>['columns'] {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,30 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
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 { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||||
import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
|
import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import { useSourceConfigColumns } from '../data';
|
||||||
|
|
||||||
const formData = ref<any[]>([]);
|
const formData = ref<any[]>([]);
|
||||||
const productList = ref<any[]>([]); // 产品列表
|
const productList = ref<any[]>([]); // 产品列表
|
||||||
const deviceList = ref<any[]>([]); // 设备列表
|
const deviceList = ref<any[]>([]); // 设备列表
|
||||||
const thingModelCache = ref<Map<number, any[]>>(new Map()); // 缓存物模型数据,key 为 productId
|
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(() => {
|
const upstreamMethods = computed(() => {
|
||||||
return Object.values(IotDeviceMessageMethodEnum).filter(
|
return Object.values(IotDeviceMessageMethodEnum).filter(
|
||||||
(item) => item.upstream,
|
(item) => item.upstream,
|
||||||
);
|
);
|
||||||
}); // 获取上行消息方法列表
|
});
|
||||||
|
|
||||||
/** 根据产品 ID 过滤设备 */
|
/** 根据产品 ID 过滤设备 */
|
||||||
function getFilteredDevices(productId: number) {
|
function getFilteredDevices(productId: number) {
|
||||||
|
|
@ -87,7 +84,6 @@ async function loadDeviceList() {
|
||||||
|
|
||||||
/** 加载物模型数据 */
|
/** 加载物模型数据 */
|
||||||
async function loadThingModel(productId: number) {
|
async function loadThingModel(productId: number) {
|
||||||
// 已缓存,无需重复加载
|
|
||||||
if (thingModelCache.value.has(productId)) {
|
if (thingModelCache.value.has(productId)) {
|
||||||
return;
|
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.deviceId = 0;
|
||||||
row.method = undefined;
|
row.method = undefined;
|
||||||
row.identifier = undefined;
|
row.identifier = undefined;
|
||||||
row.identifierLoading = false;
|
row.identifierLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 消息方法变化时处理 */
|
/** 消息方法变化时清空标识符 + 按需加载物模型 */
|
||||||
async function handleMethodChange(row: any, _index: number) {
|
async function handleMethodChange(rowIndex: number) {
|
||||||
// 清空标识符
|
const row = formData.value[rowIndex];
|
||||||
row.identifier = undefined;
|
row.identifier = undefined;
|
||||||
// 如果需要加载物模型数据
|
|
||||||
if (shouldShowIdentifierSelect(row) && row.productId) {
|
if (shouldShowIdentifierSelect(row) && row.productId) {
|
||||||
row.identifierLoading = true;
|
row.identifierLoading = true;
|
||||||
await loadThingModel(row.productId);
|
await loadThingModel(row.productId);
|
||||||
|
|
@ -119,189 +115,176 @@ async function handleMethodChange(row: any, _index: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增按钮操作 */
|
/** 表格配置 */
|
||||||
function handleAdd() {
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
const row = {
|
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,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
method: undefined,
|
method: undefined,
|
||||||
identifier: undefined,
|
identifier: undefined,
|
||||||
identifierLoading: false,
|
identifierLoading: false,
|
||||||
};
|
});
|
||||||
formData.value.push(row);
|
await reloadGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除一行数据源 */
|
||||||
function handleDelete(index: number) {
|
async function handleDelete(rowIndex: number) {
|
||||||
formData.value.splice(index, 1);
|
formData.value.splice(rowIndex, 1);
|
||||||
|
await reloadGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 表单校验 */
|
/** 校验全部行;返回 Promise,失败时 reject 第一条错误信息 */
|
||||||
function validate() {
|
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() {
|
function getData() {
|
||||||
return formData.value;
|
return formData.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置表单值 */
|
/** 设置初始数据 */
|
||||||
function setData(data: any[]) {
|
async function setData(data: any[]) {
|
||||||
// 确保每个项都有必要的字段
|
|
||||||
formData.value = (data || []).map((item) => ({
|
formData.value = (data || []).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
identifierLoading: false,
|
identifierLoading: false,
|
||||||
}));
|
}));
|
||||||
// 为已有数据预加载物模型
|
// 为已有数据预加载物模型
|
||||||
|
// TODO @AI:这里有 linter 报错:Promise returned from forEach argument is ignored
|
||||||
data?.forEach(async (item) => {
|
data?.forEach(async (item) => {
|
||||||
if (item.productId && shouldShowIdentifierSelect(item)) {
|
if (item.productId && shouldShowIdentifierSelect(item)) {
|
||||||
await loadThingModel(item.productId);
|
await loadThingModel(item.productId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await reloadGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadProductList(), loadDeviceList()]);
|
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 });
|
defineExpose({ validate, getData, setData });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Form ref="formRef" :model="{ data: formData }">
|
<div>
|
||||||
<Table
|
<Grid>
|
||||||
:columns="columns"
|
<template #productId="{ rowIndex }">
|
||||||
:data-source="formData"
|
<Select
|
||||||
:pagination="false"
|
v-model:value="formData[rowIndex].productId"
|
||||||
size="small"
|
placeholder="请选择产品"
|
||||||
bordered
|
show-search
|
||||||
>
|
:filter-option="
|
||||||
<template #bodyCell="{ column, record, index }">
|
(input: string, option: any) =>
|
||||||
<template v-if="column.dataIndex === 'productId'">
|
option.label.toLowerCase().includes(input.toLowerCase())
|
||||||
<Form.Item
|
"
|
||||||
:name="['data', index, 'productId']"
|
:options="
|
||||||
:rules="formRules.productId"
|
productList.map((p: any) => ({ label: p.name, value: p.id }))
|
||||||
class="mb-0"
|
"
|
||||||
>
|
class="w-full"
|
||||||
<Select
|
@change="() => handleProductChange(rowIndex)"
|
||||||
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>
|
|
||||||
</template>
|
</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">
|
<div class="mt-3 text-center">
|
||||||
<Button type="primary" @click="handleAdd">
|
<Button type="primary" @click="handleAdd">
|
||||||
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
||||||
添加数据源
|
添加数据源
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue