feat(运维管理): 调整系统配置,添加运维管理相关页面

- 关闭tab标签、前端更新检测(后期正式环境开启回来)
- 添加网络组件、协议管理、设备接入网关页面(未完成)和对接接口
This commit is contained in:
fhysy 2025-08-05 17:03:41 +08:00
parent f53bed6616
commit fe3c9b570e
17 changed files with 2163 additions and 0 deletions

View File

@ -0,0 +1,61 @@
import type { GatewayVO, GatewayForm, GatewayQuery } from './model';
import type { ID, IDS } from '#/api/common';
import type { PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
/**
*
* @param params
* @returns
*/
export function gatewayList(params?: GatewayQuery) {
return requestClient.get<PageResult<GatewayVO>>('/operations/gateway/list', { params });
}
/**
*
* @param params
* @returns
*/
export function gatewayExport(params?: GatewayQuery) {
return commonExport('/operations/gateway/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function gatewayInfo(id: ID) {
return requestClient.get<GatewayVO>(`/operations/gateway/${id}`);
}
/**
*
* @param data
* @returns void
*/
export function gatewayAdd(data: GatewayForm) {
return requestClient.postWithMsg<void>('/operations/gateway', data);
}
/**
*
* @param data
* @returns void
*/
export function gatewayUpdate(data: GatewayForm) {
return requestClient.putWithMsg<void>('/operations/gateway', data);
}
/**
*
* @param id id
* @returns void
*/
export function gatewayRemove(id: ID | IDS) {
return requestClient.deleteWithMsg<void>(`/operations/gateway/${id}`);
}

View File

@ -0,0 +1,159 @@
import type { PageQuery, BaseEntity } from '#/api/common';
export interface GatewayVO {
/**
*
*/
id: string | number;
/**
* 0
*/
enabled: string;
/**
*
*/
name: string;
/**
* ,: mqtt-server-gateway
*/
provider: string | number;
/**
*
*/
description: string;
/**
* (),
*/
channel: string;
/**
* id 使ID,: 网络组件ID,modbus通道ID
*/
channelId: string | number;
/**
*
*/
protocol: number;
/**
* ,TCP,MQTT,UDP
*/
transport: string;
/**
*
*/
gatewayConfig: string;
}
export interface GatewayForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 0
*/
enabled?: string;
/**
*
*/
name?: string;
/**
* ,: mqtt-server-gateway
*/
provider?: string | number;
/**
*
*/
description?: string;
/**
* (),
*/
channel?: string;
/**
* id 使ID,: 网络组件ID,modbus通道ID
*/
channelId?: string | number;
/**
*
*/
protocol?: number;
/**
* ,TCP,MQTT,UDP
*/
transport?: string;
/**
*
*/
gatewayConfig?: string;
}
export interface GatewayQuery extends PageQuery {
/**
* 0
*/
enabled?: string;
/**
*
*/
name?: string;
/**
* ,: mqtt-server-gateway
*/
provider?: string | number;
/**
*
*/
description?: string;
/**
* (),
*/
channel?: string;
/**
* id 使ID,: 网络组件ID,modbus通道ID
*/
channelId?: string | number;
/**
*
*/
protocol?: number;
/**
* ,TCP,MQTT,UDP
*/
transport?: string;
/**
*
*/
gatewayConfig?: string;
/**
*
*/
params?: any;
}

View File

@ -0,0 +1,61 @@
import type { NetworkVO, NetworkForm, NetworkQuery } from './model';
import type { ID, IDS } from '#/api/common';
import type { PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
/**
*
* @param params
* @returns
*/
export function networkList(params?: NetworkQuery) {
return requestClient.get<PageResult<NetworkVO>>('/operations/network/list', { params });
}
/**
*
* @param params
* @returns
*/
export function networkExport(params?: NetworkQuery) {
return commonExport('/operations/network/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function networkInfo(id: ID) {
return requestClient.get<NetworkVO>(`/operations/network/${id}`);
}
/**
*
* @param data
* @returns void
*/
export function networkAdd(data: NetworkForm) {
return requestClient.postWithMsg<void>('/operations/network', data);
}
/**
*
* @param data
* @returns void
*/
export function networkUpdate(data: NetworkForm) {
return requestClient.putWithMsg<void>('/operations/network', data);
}
/**
*
* @param id id
* @returns void
*/
export function networkRemove(id: ID | IDS) {
return requestClient.deleteWithMsg<void>(`/operations/network/${id}`);
}

View File

@ -0,0 +1,99 @@
import type { PageQuery, BaseEntity } from '#/api/common';
export interface NetworkVO {
/**
*
*/
id: string | number;
/**
* 0
*/
enabled: string;
/**
*
*/
name: string;
/**
* HTTP_SERVERMQTT_CLIENT
*/
networkType: string;
/**
*
*/
description: string;
/**
*
*/
networkConfig: string;
}
export interface NetworkForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 0
*/
enabled?: string;
/**
*
*/
name?: string;
/**
* HTTP_SERVERMQTT_CLIENT
*/
networkType?: string;
/**
*
*/
description?: string;
/**
*
*/
networkConfig?: string;
}
export interface NetworkQuery extends PageQuery {
/**
* 0
*/
enabled?: string;
/**
*
*/
name?: string;
/**
* HTTP_SERVERMQTT_CLIENT
*/
networkType?: string;
/**
*
*/
description?: string;
/**
*
*/
networkConfig?: string;
/**
*
*/
params?: any;
}

View File

@ -0,0 +1,61 @@
import type { ProtocolVO, ProtocolForm, ProtocolQuery } from './model';
import type { ID, IDS } from '#/api/common';
import type { PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
/**
*
* @param params
* @returns
*/
export function protocolList(params?: ProtocolQuery) {
return requestClient.get<PageResult<ProtocolVO>>('/operations/protocol/list', { params });
}
/**
*
* @param params
* @returns
*/
export function protocolExport(params?: ProtocolQuery) {
return commonExport('/operations/protocol/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function protocolInfo(id: ID) {
return requestClient.get<ProtocolVO>(`/operations/protocol/${id}`);
}
/**
*
* @param data
* @returns void
*/
export function protocolAdd(data: ProtocolForm) {
return requestClient.postWithMsg<void>('/operations/protocol', data);
}
/**
*
* @param data
* @returns void
*/
export function protocolUpdate(data: ProtocolForm) {
return requestClient.putWithMsg<void>('/operations/protocol', data);
}
/**
*
* @param id id
* @returns void
*/
export function protocolRemove(id: ID | IDS) {
return requestClient.deleteWithMsg<void>(`/operations/protocol/${id}`);
}

View File

@ -0,0 +1,99 @@
import type { PageQuery, BaseEntity } from '#/api/common';
export interface ProtocolVO {
/**
*
*/
id: string | number;
/**
* 0
*/
enabled: string;
/**
*
*/
name: string;
/**
* localjar
*/
protocolType: string;
/**
*
*/
description: string;
/**
*
*/
protocolConfig: string;
}
export interface ProtocolForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 0
*/
enabled?: string;
/**
*
*/
name?: string;
/**
* localjar
*/
protocolType?: string;
/**
*
*/
description?: string;
/**
*
*/
protocolConfig?: string;
}
export interface ProtocolQuery extends PageQuery {
/**
* 0
*/
enabled?: string;
/**
*
*/
name?: string;
/**
* localjar
*/
protocolType?: string;
/**
*
*/
description?: string;
/**
*
*/
protocolConfig?: string;
/**
*
*/
params?: any;
}

View File

@ -30,6 +30,10 @@ export const overridesPreferences = defineOverridesPreferences({
* 2. * 2.
*/ */
// loginExpiredMode: 'modal', // loginExpiredMode: 'modal',
// 是否开启检查更新
enableCheckUpdates: false,
// 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1,
}, },
footer: { footer: {
/** /**
@ -42,6 +46,8 @@ export const overridesPreferences = defineOverridesPreferences({
* tab * tab
*/ */
persist: false, persist: false,
// 隐藏tab显示
enable: false,
// styleType: 'card', // styleType: 'card',
}, },
theme: { theme: {

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
</script>
<template>
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Test Detail Page</h1>
<p>This is a placeholder for the test detail page.</p>
</div>
</template>

View File

@ -0,0 +1,170 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'enabled',
label: '启用状态 0 禁用',
},
{
component: 'Input',
fieldName: 'name',
label: '网关名称',
},
{
component: 'Input',
fieldName: 'provider',
label: '接入方式,如: mqtt-server-gateway',
},
{
component: 'Input',
fieldName: 'description',
label: '描述',
},
{
component: 'Input',
fieldName: 'channel',
label: '通道 接入通道(方式),如网络组件',
},
{
component: 'Input',
fieldName: 'channelId',
label: '通道id 接入使用的通道ID,如: 网络组件ID,modbus通道ID',
},
{
component: 'Input',
fieldName: 'protocol',
label: '消息协议 消息协议',
},
{
component: 'Input',
fieldName: 'transport',
label: '传输协议,如TCP,MQTT,UDP',
},
{
component: 'Textarea',
fieldName: 'gatewayConfig',
label: '网关配置',
},
];
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
// export const columns: () => VxeGridProps['columns'] = () => [
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '编号',
field: 'id',
},
{
title: '启用状态 0 禁用',
field: 'enabled',
},
{
title: '网关名称',
field: 'name',
},
{
title: '接入方式,如: mqtt-server-gateway',
field: 'provider',
},
{
title: '描述',
field: 'description',
},
{
title: '通道 接入通道(方式),如网络组件',
field: 'channel',
},
{
title: '通道id 接入使用的通道ID,如: 网络组件ID,modbus通道ID',
field: 'channelId',
},
{
title: '消息协议 消息协议',
field: 'protocol',
},
{
title: '传输协议,如TCP,MQTT,UDP',
field: 'transport',
},
{
title: '网关配置',
field: 'gatewayConfig',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
},
];
export const drawerSchema: FormSchemaGetter = () => [
{
label: '编号',
fieldName: 'id',
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '启用状态 0 禁用',
fieldName: 'enabled',
component: 'Input',
rules: 'required',
},
{
label: '网关名称',
fieldName: 'name',
component: 'Input',
rules: 'required',
},
{
label: '接入方式,如: mqtt-server-gateway',
fieldName: 'provider',
component: 'Input',
rules: 'required',
},
{
label: '描述',
fieldName: 'description',
component: 'Input',
rules: 'required',
},
{
label: '通道 接入通道(方式),如网络组件',
fieldName: 'channel',
component: 'Input',
rules: 'required',
},
{
label: '通道id 接入使用的通道ID,如: 网络组件ID,modbus通道ID',
fieldName: 'channelId',
component: 'Input',
rules: 'required',
},
{
label: '消息协议 消息协议',
fieldName: 'protocol',
component: 'Input',
rules: 'required',
},
{
label: '传输协议,如TCP,MQTT,UDP',
fieldName: 'transport',
component: 'Input',
rules: 'required',
},
{
label: '网关配置',
fieldName: 'gatewayConfig',
component: 'Textarea',
rules: 'required',
},
];

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { gatewayAdd, gatewayInfo, gatewayUpdate } from '#/api/operations/gateway';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
//
formItemClass: 'col-span-2',
// label px
labelWidth: 80,
//
componentProps: {
class: 'w-full',
}
},
schema: drawerSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
//
class: 'w-[550px]',
fullscreenButton: false,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
if (isUpdate.value && id) {
const record = await gatewayInfo(id);
await formApi.setValues(record);
}
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// getValuesreadonly
const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? gatewayUpdate(data) : gatewayAdd(data));
resetInitialized();
emit('reload');
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
</script>
<template>
<BasicDrawer :title="title">
<BasicForm />
</BasicDrawer>
</template>

View File

@ -0,0 +1,182 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import { ref } from 'vue';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import dayjs from 'dayjs';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps
} from '#/adapter/vxe-table';
import {
gatewayExport,
gatewayList,
gatewayRemove,
} from '#/api/operations/gateway';
import type { GatewayForm } from '#/api/operations/gateway/model';
import { commonDownloadExcel } from '#/utils/file/download';
import gatewayDrawer from './gateway-drawer.vue';
import { columns, querySchema } from './data';
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: querySchema(),
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 = {
checkboxConfig: {
//
highlight: true,
//
reserve: true,
//
// trigger: 'row',
},
// 使i18ngetter
// columns: columns(),
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
return await gatewayList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
//
id: 'operations-gateway-index'
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const [GatewayDrawer, drawerApi] = useVbenDrawer({
connectedComponent: gatewayDrawer,
});
function handleAdd() {
drawerApi.setData({});
drawerApi.open();
}
async function handleEdit(row: Required<GatewayForm>) {
drawerApi.setData({ id: row.id });
drawerApi.open();
}
async function handleDelete(row: Required<GatewayForm>) {
await gatewayRemove(row.id);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: Required<GatewayForm>) => row.id);
Modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await gatewayRemove(ids);
await tableApi.query();
},
});
}
function handleDownloadExcel() {
commonDownloadExcel(gatewayExport, '设备接入网关数据', tableApi.formApi.form.values, {
fieldMappingTime: formOptions.fieldMappingTime,
});
}
</script>
<template>
<Page :auto-content-height="true">
<BasicTable table-title="设备接入网关列表">
<template #toolbar-tools>
<Space>
<a-button
v-access:code="['operations:gateway:export']"
@click="handleDownloadExcel"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="!vxeCheckboxChecked(tableApi)"
danger
type="primary"
v-access:code="['operations:gateway:remove']"
@click="handleMultiDelete">
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['operations:gateway:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<Space>
<ghost-button
v-access:code="['operations:gateway:edit']"
@click.stop="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['operations:gateway:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
<GatewayDrawer @reload="tableApi.query()" />
</Page>
</template>

View File

@ -0,0 +1,348 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
const networkTypeOptions = [
{ label: 'MQTT客户端', value: 'MQTT_CLIENT' },
{ label: 'HTTP服务', value: 'HTTP_SERVER' },
];
const enabledOptions = [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
];
// HTTP服务配置字段
const httpServerFields = [
{
label: '本地地址',
fieldName: 'localAddress',
component: 'Input',
componentProps: {
placeholder: '请输入本地地址',
disabled: true,
},
defaultValue: '0.0.0.0',
help: '绑定到服务器上的网卡地址,绑定到所有网卡:0.0.0.0',
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'HTTP_SERVER',
},
},
{
label: '本地端口',
fieldName: 'localPort',
component: 'InputNumber',
componentProps: {
placeholder: '请选择本地端口',
min: 1,
},
rules: 'required',
help: '监听指定端口的请求',
dependencies: {
triggerFields: ['networkType'],
show: (values) => values.networkType === 'HTTP_SERVER',
},
},
{
label: '公网地址',
fieldName: 'publicAddress',
component: 'Input',
componentProps: {
placeholder: '请输入公网地址',
},
rules: 'required',
help: '对外提供访问的地址,内网环境时填写服务器的内网IP地址',
dependencies: {
triggerFields: ['networkType'],
show: (values) => values.networkType === 'HTTP_SERVER',
},
},
{
label: '公网端口',
fieldName: 'publicPort',
component: 'InputNumber',
componentProps: {
placeholder: '请输入端口',
min: 1,
},
rules: 'required',
help: '对外提供访问的端口',
dependencies: {
triggerFields: ['networkType'],
show: (values) => values.networkType === 'HTTP_SERVER',
},
},
{
label: '开启TLS',
fieldName: 'enableTls',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
optionType: 'button',
options: [
{ label: '是', value: '1' },
{ label: '否', value: '0' },
],
},
defaultValue: '0',
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values) => values.networkType === 'HTTP_SERVER',
},
},
];
// MQTT客户端配置字段
const mqttClientFields = [
{
label: '远程地址',
fieldName: 'remoteAddress',
component: 'Input',
componentProps: {
placeholder: '请输入远程地址',
},
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: '远程端口',
fieldName: 'remotePort',
component: 'InputNumber',
componentProps: {
placeholder: '请输入远程端口',
min: 1,
},
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: 'ClientId',
fieldName: 'clientId',
component: 'Input',
componentProps: {
placeholder: '请输入ClientId',
},
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: '用户名',
fieldName: 'username',
component: 'Input',
componentProps: {
placeholder: '请输入用户名',
},
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: '密码',
fieldName: 'password',
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: '订阅前缀',
fieldName: 'subscriptionPrefix',
component: 'Input',
componentProps: {
placeholder: '请输入订阅前缀',
},
help: '当连接的服务为EMQ时,可能需要使用共享的订阅前缀,如:$queue或$share',
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: '最大消息长度',
fieldName: 'maxMessageLength',
component: 'InputNumber',
componentProps: {
placeholder: '请输入最大消息长度',
min: 1,
},
defaultValue: 8192,
help: '单次收发消息的最大长度,单位:字节;设置过大可能会影响性能',
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
{
label: '开启TLS',
fieldName: 'enableTls',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
optionType: 'button',
options: [
{ label: '是', value: '1' },
{ label: '否', value: '0' },
],
},
defaultValue: '0',
rules: 'required',
dependencies: {
triggerFields: ['networkType'],
show: (values: any) => values.networkType === 'MQTT_CLIENT',
},
},
];
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'name',
label: '组件名称',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: enabledOptions,
placeholder: '请选择',
showSearch: true,
},
fieldName: 'enabled',
label: '启用状态',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: networkTypeOptions,
placeholder: '请选择',
showSearch: true,
},
fieldName: 'networkType',
label: '网络类型',
},
];
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
// export const columns: () => VxeGridProps['columns'] = () => [
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '编号',
field: 'id',
},
{
title: '组件名称',
field: 'name',
},
{
title: '网络类型',
field: 'networkType',
slots: { default: 'networkType' },
},
{
title: '启用状态',
field: 'enabled',
slots: { default: 'enabled' },
},
{
title: '描述',
field: 'description',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
},
];
// 基础字段
const baseFields = [
{
label: '编号',
fieldName: 'id',
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '组件名称',
fieldName: 'name',
component: 'Input',
componentProps: {
placeholder: '请输入名称',
},
rules: 'required',
},
{
label: '网络类型',
fieldName: 'networkType',
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: networkTypeOptions,
placeholder: '请选择网络类型',
showSearch: true,
},
rules: 'selectRequired',
},
{
label: '启用状态',
fieldName: 'enabled',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: enabledOptions,
optionType: 'button',
},
defaultValue: '1',
rules: 'required',
},
{
label: '描述',
fieldName: 'description',
component: 'Textarea',
componentProps: {
placeholder: '请输入描述',
rows: 3,
},
},
];
export const drawerSchema: FormSchemaGetter = () => [
...baseFields,
// HTTP服务配置字段
...httpServerFields,
// MQTT客户端配置字段
...mqttClientFields,
];
// 导出字段配置供组件使用
export { httpServerFields, mqttClientFields };

View File

@ -0,0 +1,230 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { NetworkForm } from '#/api/operations/network/model';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space, Tag } from 'ant-design-vue';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
networkExport,
networkList,
networkRemove,
} from '#/api/operations/network';
import { commonDownloadExcel } from '#/utils/file/download';
import { columns, querySchema } from './data';
import networkDrawer from './network-drawer.vue';
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: querySchema(),
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 = {
checkboxConfig: {
//
highlight: true,
//
reserve: true,
//
// trigger: 'row',
},
// 使i18ngetter
// columns: columns(),
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
return await networkList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
//
id: 'operations-network-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const [NetworkDrawer, drawerApi] = useVbenDrawer({
connectedComponent: networkDrawer,
});
function handleAdd() {
drawerApi.setData({});
drawerApi.open();
}
async function handleEdit(row: Required<NetworkForm>) {
drawerApi.setData({ id: row.id });
drawerApi.open();
}
async function handleDelete(row: Required<NetworkForm>) {
await networkRemove(row.id);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: Required<NetworkForm>) => row.id);
Modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await networkRemove(ids);
await tableApi.query();
},
});
}
function handleDownloadExcel() {
commonDownloadExcel(
networkExport,
'网络组件数据',
tableApi.formApi.form.values,
{
fieldMappingTime: formOptions.fieldMappingTime,
},
);
}
// JSON
function parseNetworkConfig(networkConfig: string) {
try {
return JSON.parse(networkConfig);
} catch (error) {
console.error('解析网络配置失败:', error);
return null;
}
}
//
function formatNetworkConfig(row: any) {
const config = parseNetworkConfig(row.networkConfig);
if (!config) {
return '配置解析失败';
}
if (row.networkType === 'HTTP_SERVER') {
return `本地: ${config.localAddress}:${config.localPort} | 公网: ${config.publicAddress}:${config.publicPort} | TLS: ${config.enableTls === '1' ? '是' : '否'}`;
} else if (row.networkType === 'MQTT_CLIENT') {
return `远程: ${config.remoteAddress}:${config.remotePort} | ClientId: ${config.clientId} | TLS: ${config.enableTls === '1' ? '是' : '否'}`;
}
return '未知配置';
}
</script>
<template>
<Page :auto-content-height="true">
<BasicTable table-title="网络组件列表">
<template #toolbar-tools>
<Space>
<a-button
v-access:code="['operations:network:export']"
@click="handleDownloadExcel"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="!vxeCheckboxChecked(tableApi)"
danger
type="primary"
v-access:code="['operations:network:remove']"
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['operations:network:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #networkType="{ row }">
<Tag color="processing">{{ row.networkType }}</Tag>
</template>
<template #enabled="{ row }">
<Tag :color="row.enabled === '1' ? 'success' : 'error'">
{{ row.enabled === '1' ? '启用' : '禁用' }}
</Tag>
</template>
<template #action="{ row }">
<Space>
<ghost-button
v-access:code="['operations:network:edit']"
@click.stop="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['operations:network:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
<NetworkDrawer @reload="tableApi.query()" />
</Page>
</template>
<style scoped>
.network-config-display {
display: flex;
flex-direction: column;
gap: 4px;
}
.config-summary {
font-size: 12px;
line-height: 1.4;
color: #666;
}
</style>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import {
networkAdd,
networkInfo,
networkUpdate,
} from '#/api/operations/network';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
//
formItemClass: 'col-span-2',
// label px
labelWidth: 100,
//
componentProps: {
class: 'w-full',
},
},
schema: drawerSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
//
class: 'w-[800px]',
fullscreenButton: false,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
if (isUpdate.value && id) {
const record = await networkInfo(id);
// JSON
if (record.networkConfig) {
try {
const configData = JSON.parse(record.networkConfig);
//
const formData = { ...record, ...configData };
await formApi.setValues(formData);
} catch (error) {
console.error('解析网络配置失败:', error);
await formApi.setValues(record);
}
} else {
await formApi.setValues(record);
}
} else {
//
await formApi.setValues({
networkType: 'MQTT_CLIENT',
});
}
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// getValuesreadonly
const formData = cloneDeep(await formApi.getValues());
//
const baseFields = new Set([
'description',
'enabled',
'id',
'name',
'networkType',
]);
const baseData: any = {};
const configData: any = {};
Object.keys(formData).forEach((key) => {
if (baseFields.has(key)) {
baseData[key] = formData[key];
} else {
configData[key] = formData[key];
}
});
// JSON
baseData.networkConfig = JSON.stringify(configData);
await (isUpdate.value ? networkUpdate(baseData) : networkAdd(baseData));
resetInitialized();
emit('reload');
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
</script>
<template>
<BasicDrawer :title="title">
<BasicForm />
</BasicDrawer>
</template>

View File

@ -0,0 +1,138 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
const enabledOptions = [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
];
const protocolTypeOptions = [
{ label: 'local', value: 'local' },
{ label: 'jar', value: 'jar' },
];
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'name',
label: '协议名称',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: protocolTypeOptions,
placeholder: '请选择',
showSearch: true,
},
fieldName: 'protocolType',
label: '协议包类型',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: enabledOptions,
placeholder: '请选择',
showSearch: true,
},
fieldName: 'enabled',
label: '启用状态',
},
];
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
// export const columns: () => VxeGridProps['columns'] = () => [
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '编号',
field: 'id',
},
{
title: '协议名称',
field: 'name',
},
{
title: '协议包类型',
slots: { default: 'protocolType' },
field: 'protocolType',
},
{
title: '协议配置',
field: 'protocolConfig',
},
{
title: '启用状态',
slots: { default: 'enabled' },
field: 'enabled',
},
{
title: '描述',
field: 'description',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
},
];
export const drawerSchema: FormSchemaGetter = () => [
{
label: '编号',
fieldName: 'id',
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '协议名称',
fieldName: 'name',
component: 'Input',
rules: 'required',
},
{
label: '协议包类型',
fieldName: 'protocolType',
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: protocolTypeOptions,
placeholder: '请选择',
showSearch: true,
},
rules: 'selectRequired',
},
{
label: '协议配置',
fieldName: 'protocolConfig',
component: 'Textarea',
rules: 'required',
},
{
label: '启用状态',
fieldName: 'enabled',
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: enabledOptions,
placeholder: '请选择',
showSearch: true,
},
rules: 'required',
},
{
label: '描述',
fieldName: 'description',
component: 'Textarea',
},
];

View File

@ -0,0 +1,190 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { ProtocolForm } from '#/api/operations/protocol/model';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space, Tag } from 'ant-design-vue';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
protocolExport,
protocolList,
protocolRemove,
} from '#/api/operations/protocol';
import { commonDownloadExcel } from '#/utils/file/download';
import { columns, querySchema } from './data';
import protocolDrawer from './protocol-drawer.vue';
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: querySchema(),
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 = {
checkboxConfig: {
//
highlight: true,
//
reserve: true,
//
// trigger: 'row',
},
// 使i18ngetter
// columns: columns(),
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
return await protocolList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
//
id: 'operations-protocol-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const [ProtocolDrawer, drawerApi] = useVbenDrawer({
connectedComponent: protocolDrawer,
});
function handleAdd() {
drawerApi.setData({});
drawerApi.open();
}
async function handleEdit(row: Required<ProtocolForm>) {
drawerApi.setData({ id: row.id });
drawerApi.open();
}
async function handleDelete(row: Required<ProtocolForm>) {
await protocolRemove(row.id);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: Required<ProtocolForm>) => row.id);
Modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await protocolRemove(ids);
await tableApi.query();
},
});
}
function handleDownloadExcel() {
commonDownloadExcel(
protocolExport,
'设备协议数据',
tableApi.formApi.form.values,
{
fieldMappingTime: formOptions.fieldMappingTime,
},
);
}
</script>
<template>
<Page :auto-content-height="true">
<BasicTable table-title="设备协议列表">
<template #toolbar-tools>
<Space>
<a-button
v-access:code="['operations:protocol:export']"
@click="handleDownloadExcel"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="!vxeCheckboxChecked(tableApi)"
danger
type="primary"
v-access:code="['operations:protocol:remove']"
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['operations:protocol:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #protocolType="{ row }">
<Tag color="processing">{{ row.protocolType }}</Tag>
</template>
<template #enabled="{ row }">
<Tag :color="row.enabled === '1' ? 'success' : 'error'">
{{ row.enabled === '1' ? '启用' : '禁用' }}
</Tag>
</template>
<template #action="{ row }">
<Space>
<ghost-button
v-access:code="['operations:protocol:edit']"
@click.stop="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['operations:protocol:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
<ProtocolDrawer @reload="tableApi.query()" />
</Page>
</template>

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { protocolAdd, protocolInfo, protocolUpdate } from '#/api/operations/protocol';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
//
formItemClass: 'col-span-2',
// label px
labelWidth: 80,
//
componentProps: {
class: 'w-full',
}
},
schema: drawerSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
//
class: 'w-[550px]',
fullscreenButton: false,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
if (isUpdate.value && id) {
const record = await protocolInfo(id);
await formApi.setValues(record);
}
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// getValuesreadonly
const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? protocolUpdate(data) : protocolAdd(data));
resetInitialized();
emit('reload');
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
</script>
<template>
<BasicDrawer :title="title">
<BasicForm />
</BasicDrawer>
</template>