feat(运维管理): 重构网关页面并添加新功能

-重新设计了网关列表和表单的结构,增加了更多字段和选项
- 添加了网络组件和消息协议的动态加载功能
- 实现了根据接入方式动态显示不同表单字段的逻辑
- 优化了表单验证和数据提交的流程
- 统一了启用状态的显示和操作
- 调整了部分UI样式,提高了用户体验
This commit is contained in:
fhysy 2025-08-07 08:43:01 +08:00
parent fe3c9b570e
commit cb5ab82af4
6 changed files with 443 additions and 131 deletions

View File

@ -1,52 +1,122 @@
import type { FormSchemaGetter } from '#/adapter/form'; import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
import { networkList } from '#/api/operations/network';
import { protocolList } from '#/api/operations/protocol';
import { enabledOptions, networkTypeOptions } from '../user.data';
export const providerOptions = [
...networkTypeOptions,
{
label: '网关子设备接入',
value: 'child-device',
channel: 'child-device',
transport: 'Gateway',
description:
'需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
},
];
// 存储网络组件更新回调函数
let networkOptionsUpdateCallback: ((provider: string) => Promise<void>) | null =
null;
// 注册网络组件更新回调
export const registerNetworkOptionsUpdate = (
callback: (provider: string) => Promise<void>,
) => {
networkOptionsUpdateCallback = callback;
};
// 获取网络组件选项
export const getNetworkOptions = async (networkType?: string) => {
try {
const params: any = { enabled: '1' };
if (networkType) {
params.networkType = networkType;
}
const response = await networkList(params);
return response.rows.map((item: any) => ({
label: item.name,
value: item.id,
}));
} catch (error) {
console.error('获取网络组件失败:', error);
return [];
}
};
// 获取消息协议选项
export const getProtocolOptions = async () => {
try {
const response = await protocolList({ enabled: '1' });
return response.rows.map((item: any) => ({
label: item.name,
value: item.id,
}));
} catch (error) {
console.error('获取消息协议失败:', error);
return [];
}
};
export const querySchema: FormSchemaGetter = () => [ export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'enabled',
label: '启用状态 0 禁用',
},
{ {
component: 'Input', component: 'Input',
fieldName: 'name', fieldName: 'name',
label: '网关名称', label: '网关名称',
}, },
{ {
component: 'Input', component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: providerOptions,
placeholder: '请选择',
showSearch: true,
},
fieldName: 'provider', fieldName: 'provider',
label: '接入方式,如: mqtt-server-gateway', label: '接入方式',
}, },
// {
// component: 'Select',
// componentProps: {
// allowClear: true,
// filterOption: true,
// placeholder: '请选择网络组件',
// showSearch: true,
// },
// fieldName: 'channelId',
// label: '网络组件',
// dependencies: {
// triggerFields: ['provider'],
// show: (values: any) =>
// values.provider && values.provider !== 'child-device',
// },
// },
{ {
component: 'Input', component: 'Select',
fieldName: 'description', componentProps: {
label: '描述', allowClear: true,
filterOption: true,
placeholder: '请选择消息协议',
showSearch: true,
}, },
{
component: 'Input',
fieldName: 'channel',
label: '通道 接入通道(方式),如网络组件',
},
{
component: 'Input',
fieldName: 'channelId',
label: '通道id 接入使用的通道ID,如: 网络组件ID,modbus通道ID',
},
{
component: 'Input',
fieldName: 'protocol', fieldName: 'protocol',
label: '消息协议 消息协议', label: '消息协议',
}, },
{ {
component: 'Input', component: 'Select',
fieldName: 'transport', componentProps: {
label: '传输协议,如TCP,MQTT,UDP', allowClear: true,
filterOption: true,
options: enabledOptions,
placeholder: '请选择',
showSearch: true,
}, },
{ fieldName: 'enabled',
component: 'Textarea', label: '启用状态',
fieldName: 'gatewayConfig',
label: '网关配置',
}, },
]; ];
@ -58,41 +128,22 @@ export const columns: VxeGridProps['columns'] = [
title: '编号', title: '编号',
field: 'id', field: 'id',
}, },
{
title: '启用状态 0 禁用',
field: 'enabled',
},
{ {
title: '网关名称', title: '网关名称',
field: 'name', field: 'name',
}, },
{ {
title: '接入方式,如: mqtt-server-gateway', title: '网络组件',
field: 'provider',
},
{
title: '描述',
field: 'description',
},
{
title: '通道 接入通道(方式),如网络组件',
field: 'channel',
},
{
title: '通道id 接入使用的通道ID,如: 网络组件ID,modbus通道ID',
field: 'channelId', field: 'channelId',
}, },
{ {
title: '消息协议 消息协议', title: '消息协议',
field: 'protocol', field: 'protocol',
}, },
{ {
title: '传输协议,如TCP,MQTT,UDP', title: '启用状态',
field: 'transport', field: 'enabled',
}, slots: { default: 'enabled' },
{
title: '网关配置',
field: 'gatewayConfig',
}, },
{ {
field: 'action', field: 'action',
@ -113,12 +164,6 @@ export const drawerSchema: FormSchemaGetter = () => [
triggerFields: [''], triggerFields: [''],
}, },
}, },
{
label: '启用状态 0 禁用',
fieldName: 'enabled',
component: 'Input',
rules: 'required',
},
{ {
label: '网关名称', label: '网关名称',
fieldName: 'name', fieldName: 'name',
@ -126,45 +171,94 @@ export const drawerSchema: FormSchemaGetter = () => [
rules: 'required', rules: 'required',
}, },
{ {
label: '接入方式,如: mqtt-server-gateway', label: '接入方式',
fieldName: 'provider', fieldName: 'provider',
component: 'Input', component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: providerOptions,
placeholder: '请选择',
showSearch: true,
onChange: async (value: string) => {
console.log('抽屉表单接入方式变化:', value);
if (networkOptionsUpdateCallback) {
await networkOptionsUpdateCallback(value);
}
},
},
rules: 'required',
},
{
label: '网络组件',
fieldName: 'channelId',
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
placeholder: '请选择网络组件',
showSearch: true,
},
rules: 'required',
dependencies: {
triggerFields: ['provider'],
show: (values: any) =>
values.provider && values.provider !== 'child-device',
},
},
{
label: '消息协议',
fieldName: 'protocol',
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
placeholder: '请选择消息协议',
showSearch: true,
},
rules: 'required',
dependencies: {
triggerFields: ['provider'],
show: (values: any) => values.provider && values.provider !== '',
},
},
{
label: '启用状态',
fieldName: 'enabled',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: enabledOptions,
optionType: 'button',
},
defaultValue: '1',
rules: 'required', rules: 'required',
}, },
{ {
label: '描述', label: '描述',
fieldName: 'description', fieldName: 'description',
component: 'Input', component: 'Textarea',
rules: 'required', componentProps: {
placeholder: '请输入描述',
rows: 3,
},
}, },
{ {
label: '通道 接入通道(方式),如网络组件', label: '接入通道(方式)',
fieldName: 'channel', fieldName: 'channel',
component: 'Input', component: 'Input',
rules: 'required', dependencies: {
show: () => false,
triggerFields: [''],
},
}, },
{ {
label: '通道id 接入使用的通道ID,如: 网络组件ID,modbus通道ID', label: '传输协议',
fieldName: 'channelId',
component: 'Input',
rules: 'required',
},
{
label: '消息协议 消息协议',
fieldName: 'protocol',
component: 'Input',
rules: 'required',
},
{
label: '传输协议,如TCP,MQTT,UDP',
fieldName: 'transport', fieldName: 'transport',
component: 'Input', component: 'Input',
rules: 'required', dependencies: {
show: () => false,
triggerFields: [''],
}, },
{
label: '网关配置',
fieldName: 'gatewayConfig',
component: 'Textarea',
rules: 'required',
}, },
]; ];

View File

@ -6,10 +6,20 @@ import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils'; import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { gatewayAdd, gatewayInfo, gatewayUpdate } from '#/api/operations/gateway'; import {
gatewayAdd,
gatewayInfo,
gatewayUpdate,
} from '#/api/operations/gateway';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup'; import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import {
drawerSchema,
getNetworkOptions,
getProtocolOptions,
providerOptions,
registerNetworkOptionsUpdate,
} from './data';
const emit = defineEmits<{ reload: [] }>(); const emit = defineEmits<{ reload: [] }>();
@ -27,13 +37,72 @@ const [BasicForm, formApi] = useVbenForm({
// //
componentProps: { componentProps: {
class: 'w-full', class: 'w-full',
} },
}, },
schema: drawerSchema(), schema: drawerSchema(),
showDefaultActions: false, showDefaultActions: false,
wrapperClass: 'grid-cols-2', wrapperClass: 'grid-cols-2',
}); });
//
async function updateDrawerNetworkOptions(provider: string) {
try {
if (provider && provider !== 'child-device') {
const networkOptions = await getNetworkOptions(provider);
console.log('网络组件选项:', provider);
await formApi.updateSchema(
drawerSchema().map((field: any) => {
if (field.fieldName === 'channelId') {
return {
...field,
componentProps: {
...field.componentProps,
options: networkOptions,
},
};
}
return field;
}),
);
await formApi.setFieldValue('channelId', ''); //
providerOptions.forEach((option) => {
if (option.value === provider) {
formApi.setFieldValue('channel', option.channel);
formApi.setFieldValue('transport', option.transport);
}
});
} else {
//
await formApi.updateSchema(
drawerSchema().map((field: any) => {
if (field.fieldName === 'channelId') {
return {
...field,
componentProps: {
...field.componentProps,
options: [],
},
};
}
return field;
}),
);
await formApi.setFieldValue('channelId', ''); //
providerOptions.forEach((option) => {
if (option.value === provider) {
formApi.setFieldValue('channel', option.channel);
formApi.setFieldValue('transport', option.transport);
}
});
}
} catch (error) {
console.error('更新抽屉表单网络组件选项失败:', error);
}
}
//
registerNetworkOptionsUpdate(updateDrawerNetworkOptions);
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff( const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{ {
initializedGetter: defaultFormValueGetter(formApi), initializedGetter: defaultFormValueGetter(formApi),
@ -44,7 +113,6 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
// //
class: 'w-[550px]', class: 'w-[550px]',
fullscreenButton: false,
onBeforeClose, onBeforeClose,
onClosed: handleClosed, onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
@ -54,12 +122,39 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
// schema
try {
const protocolOptions = await getProtocolOptions();
const updatedSchema = drawerSchema().map((field: any) => {
if (field.fieldName === 'protocol') {
return {
...field,
componentProps: {
...field.componentProps,
options: protocolOptions,
},
};
}
return field;
});
await formApi.updateSchema(updatedSchema);
} catch (error) {
console.error('加载选项数据失败:', error);
}
const { id } = drawerApi.getData() as { id?: number | string }; const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
if (isUpdate.value && id) { if (isUpdate.value && id) {
const record = await gatewayInfo(id); const record = await gatewayInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
// provider
if (record.provider && record.provider !== 'child-device') {
await loadNetworkOptions(String(record.provider));
}
} }
await markInitialized(); await markInitialized();
@ -67,6 +162,32 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
}, },
}); });
//
async function loadNetworkOptions(provider: string) {
try {
if (provider && provider !== 'child-device') {
const networkOptions = await getNetworkOptions(provider);
await formApi.updateSchema(
drawerSchema().map((field: any) => {
if (field.fieldName === 'channelId') {
return {
...field,
componentProps: {
...field.componentProps,
options: networkOptions,
},
};
}
return field;
}),
);
}
} catch (error) {
console.error('加载网络组件选项失败:', error);
}
}
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.lock(true); drawerApi.lock(true);
@ -98,4 +219,3 @@ async function handleClosed() {
<BasicForm /> <BasicForm />
</BasicDrawer> </BasicDrawer>
</template> </template>

View File

@ -1,30 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Recordable } from '@vben/types'; import type { VbenFormProps } from '@vben/common-ui';
import { ref } from 'vue'; import type { VxeGridProps } from '#/adapter/vxe-table';
import type { GatewayForm } from '#/api/operations/gateway/model';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui'; import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils'; import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue'; import { Modal, Popconfirm, Space, Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import { import {
gatewayExport, gatewayExport,
gatewayList, gatewayList,
gatewayRemove, gatewayRemove,
} from '#/api/operations/gateway'; } from '#/api/operations/gateway';
import type { GatewayForm } from '#/api/operations/gateway/model';
import { commonDownloadExcel } from '#/utils/file/download'; import { commonDownloadExcel } from '#/utils/file/download';
import {
columns,
getNetworkOptions,
getProtocolOptions,
querySchema,
registerNetworkOptionsUpdate,
} from './data';
import gatewayDrawer from './gateway-drawer.vue'; import gatewayDrawer from './gateway-drawer.vue';
import { columns, querySchema } from './data';
const formOptions: VbenFormProps = { const formOptions: VbenFormProps = {
commonConfig: { commonConfig: {
@ -35,15 +35,6 @@ const formOptions: VbenFormProps = {
}, },
schema: querySchema(), schema: querySchema(),
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4', wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
// RangePicker /
//
// fieldMappingTime: [
// [
// 'createTime',
// ['params[beginTime]', 'params[endTime]'],
// ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
// ],
// ],
}; };
const gridOptions: VxeGridProps = { const gridOptions: VxeGridProps = {
@ -76,7 +67,7 @@ const gridOptions: VxeGridProps = {
keyField: 'id', keyField: 'id',
}, },
// //
id: 'operations-gateway-index' id: 'operations-gateway-index',
}; };
const [BasicTable, tableApi] = useVbenVxeGrid({ const [BasicTable, tableApi] = useVbenVxeGrid({
@ -84,6 +75,79 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions, gridOptions,
}); });
//
async function updateNetworkComponentOptions(provider: string) {
try {
if (provider && provider !== 'child-device') {
const networkOptions = await getNetworkOptions(provider);
await tableApi.formApi.updateSchema(
querySchema().map((field: any) => {
if (field.fieldName === 'channelId') {
return {
...field,
componentProps: {
...field.componentProps,
options: networkOptions,
},
};
}
return field;
}),
);
} else {
//
await tableApi.formApi.updateSchema(
querySchema().map((field: any) => {
if (field.fieldName === 'channelId') {
return {
...field,
componentProps: {
...field.componentProps,
options: [],
},
};
}
return field;
}),
);
}
} catch (error) {
console.error('更新网络组件选项失败:', error);
}
}
//
registerNetworkOptionsUpdate(updateNetworkComponentOptions);
//
async function loadFormOptions() {
try {
const protocolOptions = await getProtocolOptions();
//
await tableApi.formApi.updateSchema(
querySchema().map((field: any) => {
if (field.fieldName === 'protocol') {
return {
...field,
componentProps: {
...field.componentProps,
options: protocolOptions,
},
};
}
return field;
}),
);
} catch (error) {
console.error('加载表单选项失败:', error);
}
}
//
loadFormOptions();
const [GatewayDrawer, drawerApi] = useVbenDrawer({ const [GatewayDrawer, drawerApi] = useVbenDrawer({
connectedComponent: gatewayDrawer, connectedComponent: gatewayDrawer,
}); });
@ -118,9 +182,14 @@ function handleMultiDelete() {
} }
function handleDownloadExcel() { function handleDownloadExcel() {
commonDownloadExcel(gatewayExport, '设备接入网关数据', tableApi.formApi.form.values, { commonDownloadExcel(
gatewayExport,
'设备接入网关数据',
tableApi.formApi.form.values,
{
fieldMappingTime: formOptions.fieldMappingTime, fieldMappingTime: formOptions.fieldMappingTime,
}); },
);
} }
</script> </script>
@ -140,7 +209,8 @@ function handleDownloadExcel() {
danger danger
type="primary" type="primary"
v-access:code="['operations:gateway:remove']" v-access:code="['operations:gateway:remove']"
@click="handleMultiDelete"> @click="handleMultiDelete"
>
{{ $t('pages.common.delete') }} {{ $t('pages.common.delete') }}
</a-button> </a-button>
<a-button <a-button
@ -152,6 +222,11 @@ function handleDownloadExcel() {
</a-button> </a-button>
</Space> </Space>
</template> </template>
<template #enabled="{ row }">
<Tag :color="row.enabled === '1' ? 'success' : 'error'">
{{ row.enabled === '1' ? '启用' : '禁用' }}
</Tag>
</template>
<template #action="{ row }"> <template #action="{ row }">
<Space> <Space>
<ghost-button <ghost-button

View File

@ -1,15 +1,16 @@
import type { FormSchemaGetter } from '#/adapter/form'; import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
import { networkTypeOptions,enabledOptions } from '../user.data';
const networkTypeOptions = [ // const networkTypeOptions = [
{ label: 'MQTT客户端', value: 'MQTT_CLIENT' }, // { label: 'MQTT客户端', value: 'MQTT_CLIENT' },
{ label: 'HTTP服务', value: 'HTTP_SERVER' }, // { label: 'HTTP服务', value: 'HTTP_SERVER' },
]; // ];
const enabledOptions = [ // const enabledOptions = [
{ label: '启用', value: '1' }, // { label: '启用', value: '1' },
{ label: '禁用', value: '0' }, // { label: '禁用', value: '0' },
]; // ];
// HTTP服务配置字段 // HTTP服务配置字段
const httpServerFields = [ const httpServerFields = [

View File

@ -27,7 +27,7 @@ const [BasicForm, formApi] = useVbenForm({
// //
formItemClass: 'col-span-2', formItemClass: 'col-span-2',
// label px // label px
labelWidth: 100, labelWidth: 120,
// //
componentProps: { componentProps: {
class: 'w-full', class: 'w-full',

View File

@ -0,0 +1,22 @@
export const networkTypeOptions = [
{
label: 'MQTT客户端',
value: 'MQTT_CLIENT',
channel: 'network',
transport: 'MQTT',
description:
'MQTT是ISO 标准下基于发布/订阅范式的消息协议具有轻量、简单、开放和易于实现的特点。平台使用指定的ID接入其他远程平台订阅消息。也可添加用户名和密码校验。可设置最大消息长度。可统一设置共享的订阅前缀。',
},
{
label: 'HTTP服务',
value: 'HTTP_SERVER',
channel: 'network',
transport: 'HTTP',
description:
'HTTP服务是一个简单的请求-响应的基于TCP的无状态协议。设备通过HTTP服务与平台进行灵活的短链接通信仅支持设备和平台之间单对单的请求-响应模式',
},
];
export const enabledOptions = [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
];