From 38597dd19d073fd7b4a4d2345b562f13390ff750 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 09:19:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(iot)=EF=BC=9A=E5=A2=9E=E5=8A=A0=20modb?= =?UTF-8?q?us=20=E9=85=8D=E7=BD=AE=2050%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/iot/device/modbus/config/index.ts | 30 ++ .../src/api/iot/device/modbus/point/index.ts | 56 +++ .../src/api/iot/product/product/index.ts | 23 +- .../views/iot/device/device/detail/index.vue | 20 +- .../detail/modules/modbus-config-form.vue | 225 +++++++++++ .../device/detail/modules/modbus-config.vue | 367 ++++++++++++++++++ .../detail/modules/modbus-point-form.vue | 365 +++++++++++++++++ .../src/views/iot/product/product/data.ts | 22 +- .../product/product/detail/modules/info.vue | 13 +- .../web-antd/src/views/iot/utils/constants.ts | 132 +++++++ packages/constants/src/dict-enum.ts | 4 +- 11 files changed, 1245 insertions(+), 12 deletions(-) create mode 100644 apps/web-antd/src/api/iot/device/modbus/config/index.ts create mode 100644 apps/web-antd/src/api/iot/device/modbus/point/index.ts create mode 100644 apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue create mode 100644 apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue create mode 100644 apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue diff --git a/apps/web-antd/src/api/iot/device/modbus/config/index.ts b/apps/web-antd/src/api/iot/device/modbus/config/index.ts new file mode 100644 index 000000000..3ee1e4f10 --- /dev/null +++ b/apps/web-antd/src/api/iot/device/modbus/config/index.ts @@ -0,0 +1,30 @@ +import { requestClient } from '#/api/request'; + +export namespace IotDeviceModbusConfigApi { + /** Modbus 连接配置 VO */ + export interface ModbusConfig { + id?: number; // 主键 + deviceId: number; // 设备编号 + ip: string; // Modbus 服务器 IP 地址 + port: number; // Modbus 服务器端口 + slaveId: number; // 从站地址 + timeout: number; // 连接超时时间,单位:毫秒 + retryInterval: number; // 重试间隔,单位:毫秒 + mode: number; // 模式 + frameFormat: number; // 帧格式 + status: number; // 状态 + } +} + +/** 获取设备的 Modbus 连接配置 */ +export function getModbusConfig(deviceId: number) { + return requestClient.get( + '/iot/device-modbus-config/get', + { params: { deviceId } }, + ); +} + +/** 保存 Modbus 连接配置 */ +export function saveModbusConfig(data: IotDeviceModbusConfigApi.ModbusConfig) { + return requestClient.post('/iot/device-modbus-config/save', data); +} diff --git a/apps/web-antd/src/api/iot/device/modbus/point/index.ts b/apps/web-antd/src/api/iot/device/modbus/point/index.ts new file mode 100644 index 000000000..fa5c6767d --- /dev/null +++ b/apps/web-antd/src/api/iot/device/modbus/point/index.ts @@ -0,0 +1,56 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace IotDeviceModbusPointApi { + /** Modbus 点位配置 VO */ + export interface ModbusPoint { + id?: number; // 主键 + deviceId: number; // 设备编号 + thingModelId?: number; // 物模型属性编号 + identifier: string; // 属性标识符 + name: string; // 属性名称 + functionCode?: number; // Modbus 功能码 + registerAddress?: number; // 寄存器起始地址 + registerCount?: number; // 寄存器数量 + byteOrder?: string; // 字节序 + rawDataType?: string; // 原始数据类型 + scale: number; // 缩放因子 + pollInterval: number; // 轮询间隔,单位:毫秒 + status: number; // 状态 + } +} + +/** 获取设备的 Modbus 点位分页 */ +export function getModbusPointPage(params: PageParam) { + return requestClient.get>( + '/iot/device-modbus-point/page', + { params }, + ); +} + +/** 获取 Modbus 点位详情 */ +export function getModbusPoint(id: number) { + return requestClient.get( + `/iot/device-modbus-point/get?id=${id}`, + ); +} + +/** 创建 Modbus 点位配置 */ +export function createModbusPoint( + data: IotDeviceModbusPointApi.ModbusPoint, +) { + return requestClient.post('/iot/device-modbus-point/create', data); +} + +/** 更新 Modbus 点位配置 */ +export function updateModbusPoint( + data: IotDeviceModbusPointApi.ModbusPoint, +) { + return requestClient.put('/iot/device-modbus-point/update', data); +} + +/** 删除 Modbus 点位配置 */ +export function deleteModbusPoint(id: number) { + return requestClient.delete(`/iot/device-modbus-point/delete?id=${id}`); +} diff --git a/apps/web-antd/src/api/iot/product/product/index.ts b/apps/web-antd/src/api/iot/product/product/index.ts index 335819dd3..f77255f4d 100644 --- a/apps/web-antd/src/api/iot/product/product/index.ts +++ b/apps/web-antd/src/api/iot/product/product/index.ts @@ -10,7 +10,7 @@ export namespace IotProductApi { productKey?: string; // 产品标识 productSecret?: string; // 产品密钥 protocolId?: number; // 协议编号 - protocolType?: number; // 接入协议类型 + protocolType?: string; // 协议类型 categoryId?: number; // 产品所属品类标识符 categoryName?: string; // 产品所属品类名称 icon?: string; // 产品图标 @@ -19,7 +19,7 @@ export namespace IotProductApi { status?: number; // 产品状态 deviceType?: number; // 设备类型 netType?: number; // 联网方式 - codecType?: string; // 数据格式(编解码器类型) + serializeType?: string; // 序列化类型 dataFormat?: number; // 数据格式 validateType?: number; // 认证方式 registerEnabled?: boolean; // 是否开启动态注册 @@ -28,6 +28,25 @@ export namespace IotProductApi { } } +// IoT 协议类型枚举 +export enum ProtocolTypeEnum { + COAP = 'coap', + EMQX = 'emqx', + HTTP = 'http', + MODBUS_TCP_CLIENT = 'modbus_tcp_client', + MODBUS_TCP_SERVER = 'modbus_tcp_server', + MQTT = 'mqtt', + TCP = 'tcp', + UDP = 'udp', + WEBSOCKET = 'websocket', +} + +// IoT 序列化类型枚举 +export enum SerializeTypeEnum { + BINARY = 'binary', + JSON = 'json', +} + /** 查询产品分页 */ export function getProductPage(params: PageParam) { return requestClient.get>( diff --git a/apps/web-antd/src/views/iot/device/device/detail/index.vue b/apps/web-antd/src/views/iot/device/device/detail/index.vue index 4b549a363..cbe9319e7 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/index.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/index.vue @@ -12,13 +12,14 @@ import { DeviceTypeEnum } from '@vben/constants'; import { message, Tabs } from 'ant-design-vue'; import { getDevice } from '#/api/iot/device/device'; -import { getProduct } from '#/api/iot/product/product'; +import { getProduct, ProtocolTypeEnum } from '#/api/iot/product/product'; import { getThingModelListByProductId } from '#/api/iot/thingmodel'; import DeviceDetailConfig from './modules/config.vue'; import DeviceDetailsHeader from './modules/header.vue'; import DeviceDetailsInfo from './modules/info.vue'; import DeviceDetailsMessage from './modules/message.vue'; +import DeviceModbusConfig from './modules/modbus-config.vue'; import DeviceDetailsSimulator from './modules/simulator.vue'; import DeviceDetailsSubDevice from './modules/sub-device.vue'; import DeviceDetailsThingModel from './modules/thing-model.vue'; @@ -141,6 +142,23 @@ onMounted(async () => { @success="() => getDeviceData(id)" /> + + + diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue new file mode 100644 index 000000000..56b05adbb --- /dev/null +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue @@ -0,0 +1,225 @@ + + + + diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue new file mode 100644 index 000000000..0b4648302 --- /dev/null +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue @@ -0,0 +1,367 @@ + + + + diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue new file mode 100644 index 000000000..b17c427d5 --- /dev/null +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue @@ -0,0 +1,365 @@ + + + + diff --git a/apps/web-antd/src/views/iot/product/product/data.ts b/apps/web-antd/src/views/iot/product/product/data.ts index fd02e9f09..f2c8fcb6c 100644 --- a/apps/web-antd/src/views/iot/product/product/data.ts +++ b/apps/web-antd/src/views/iot/product/product/data.ts @@ -127,16 +127,26 @@ export function useBasicFormSchema( rules: 'required', }, { - fieldName: 'codecType', - label: '数据格式', - component: 'RadioGroup', + fieldName: 'protocolType', + label: '协议类型', + component: 'Select', componentProps: { - options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'), - buttonStyle: 'solid', - optionType: 'button', + options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'string'), + placeholder: '请选择协议类型', }, rules: 'required', }, + { + fieldName: 'serializeType', + label: '序列化类型', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.IOT_SERIALIZE_TYPE, 'string'), + placeholder: '请选择序列化类型', + }, + help: 'iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型', + rules: 'required', + }, // TODO @haohao:这个貌似不需要?! { fieldName: 'status', diff --git a/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue b/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue index 674a77284..74f1156ac 100644 --- a/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue +++ b/apps/web-antd/src/views/iot/product/product/detail/modules/info.vue @@ -57,8 +57,17 @@ async function copyToClipboard(text: string) { {{ formatDate(product.createTime) }} - - {{ product.codecType || '-' }} + + + + + diff --git a/apps/web-antd/src/views/iot/utils/constants.ts b/apps/web-antd/src/views/iot/utils/constants.ts index ef7a8a6df..c66c9277e 100644 --- a/apps/web-antd/src/views/iot/utils/constants.ts +++ b/apps/web-antd/src/views/iot/utils/constants.ts @@ -522,3 +522,135 @@ export const JSON_PARAMS_EXAMPLE_VALUES: Record = { [IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] }, DEFAULT: { display: '""', value: '' }, }; + +// ========== Modbus 通用常量 ========== + +/** Modbus 模式枚举 */ +export const ModbusModeEnum = { + POLLING: 1, // 云端轮询 + ACTIVE_REPORT: 2, // 主动上报 +} as const; + +/** Modbus 帧格式枚举 */ +export const ModbusFrameFormatEnum = { + MODBUS_TCP: 1, // Modbus TCP + MODBUS_RTU: 2, // Modbus RTU +} as const; + +/** Modbus 功能码枚举 */ +export const ModbusFunctionCodeEnum = { + READ_COILS: 1, // 读线圈 + READ_DISCRETE_INPUTS: 2, // 读离散输入 + READ_HOLDING_REGISTERS: 3, // 读保持寄存器 + READ_INPUT_REGISTERS: 4, // 读输入寄存器 +} as const; + +/** Modbus 功能码选项 */ +export const ModbusFunctionCodeOptions = [ + { value: 1, label: '01 - 读线圈 (Coils)', description: '可读写布尔值' }, + { + value: 2, + label: '02 - 读离散输入 (Discrete Inputs)', + description: '只读布尔值', + }, + { + value: 3, + label: '03 - 读保持寄存器 (Holding Registers)', + description: '可读写 16 位数据', + }, + { + value: 4, + label: '04 - 读输入寄存器 (Input Registers)', + description: '只读 16 位数据', + }, +]; + +/** Modbus 原始数据类型枚举 */ +export const ModbusRawDataTypeEnum = { + INT16: 'INT16', + UINT16: 'UINT16', + INT32: 'INT32', + UINT32: 'UINT32', + FLOAT: 'FLOAT', + DOUBLE: 'DOUBLE', + BOOLEAN: 'BOOLEAN', + STRING: 'STRING', +} as const; + +/** Modbus 原始数据类型选项 */ +export const ModbusRawDataTypeOptions = [ + { + value: 'INT16', + label: 'INT16', + description: '有符号16位整数', + registerCount: 1, + }, + { + value: 'UINT16', + label: 'UINT16', + description: '无符号16位整数', + registerCount: 1, + }, + { + value: 'INT32', + label: 'INT32', + description: '有符号32位整数', + registerCount: 2, + }, + { + value: 'UINT32', + label: 'UINT32', + description: '无符号32位整数', + registerCount: 2, + }, + { + value: 'FLOAT', + label: 'FLOAT', + description: '32位浮点数', + registerCount: 2, + }, + { + value: 'DOUBLE', + label: 'DOUBLE', + description: '64位浮点数', + registerCount: 4, + }, + { + value: 'BOOLEAN', + label: 'BOOLEAN', + description: '布尔值', + registerCount: 1, + }, + { + value: 'STRING', + label: 'STRING', + description: '字符串', + registerCount: 0, + }, +]; + +/** Modbus 字节序选项 - 16位 */ +export const ModbusByteOrder16Options = [ + { value: 'AB', label: 'AB', description: '大端序' }, + { value: 'BA', label: 'BA', description: '小端序' }, +]; + +/** Modbus 字节序选项 - 32位 */ +export const ModbusByteOrder32Options = [ + { value: 'ABCD', label: 'ABCD', description: '大端序' }, + { value: 'CDAB', label: 'CDAB', description: '大端字交换' }, + { value: 'DCBA', label: 'DCBA', description: '小端序' }, + { value: 'BADC', label: 'BADC', description: '小端字交换' }, +]; + +/** 根据数据类型获取字节序选项 */ +export const getByteOrderOptions = (rawDataType: string) => { + if (['FLOAT', 'INT32', 'UINT32'].includes(rawDataType)) { + return ModbusByteOrder32Options; + } + if (rawDataType === 'DOUBLE') { + // 64 位暂时复用 32 位字节序 + return ModbusByteOrder32Options; + } + return ModbusByteOrder16Options; +}; diff --git a/packages/constants/src/dict-enum.ts b/packages/constants/src/dict-enum.ts index 73a5c57dc..e9477615b 100644 --- a/packages/constants/src/dict-enum.ts +++ b/packages/constants/src/dict-enum.ts @@ -150,7 +150,7 @@ const AI_DICT = { const IOT_DICT = { IOT_ALERT_LEVEL: 'iot_alert_level', // IoT 告警级别 IOT_ALERT_RECEIVE_TYPE: 'iot_alert_receive_type', // IoT 告警接收类型 - IOT_CODEC_TYPE: 'iot_codec_type', // IOT 数据格式(编解码器类型) + IOT_SERIALIZE_TYPE: 'iot_serialize_type', // IOT 序列化类型 IOT_DATA_FORMAT: 'iot_data_format', // IOT 数据格式 IOT_DATA_SINK_TYPE_ENUM: 'iot_data_sink_type_enum', // IoT 数据流转目的类型 IOT_DATA_TYPE: 'iot_data_type', // IOT 数据类型 @@ -171,6 +171,8 @@ const IOT_DICT = { IOT_THING_MODEL_UNIT: 'iot_thing_model_unit', // IOT 物模型单位 IOT_UNIT_TYPE: 'iot_unit_type', // IOT 单位类型 IOT_VALIDATE_TYPE: 'iot_validate_type', // IOT 数据校验级别 + IOT_MODBUS_MODE: 'iot_modbus_mode', // IoT Modbus 工作模式 + IOT_MODBUS_FRAME_FORMAT: 'iot_modbus_frame_format', // IoT Modbus 帧格式 } as const; /** 字典类型枚举 - 统一导出 */ From 63743b692922e5a1319303fb015636a0c429ea73 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 11:02:56 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(iot)=EF=BC=9A=E5=A2=9E=E5=8A=A0=20modb?= =?UTF-8?q?us=20=E9=85=8D=E7=BD=AE=20100%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/modules/modbus-config-form.vue | 41 +++-- .../device/detail/modules/modbus-config.vue | 17 +- .../detail/modules/modbus-point-form.vue | 155 ++++++------------ 3 files changed, 75 insertions(+), 138 deletions(-) diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue index 56b05adbb..fb792d4e7 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue @@ -13,6 +13,7 @@ import { message } from 'ant-design-vue'; import { useVbenForm, z } from '#/adapter/form'; import { saveModbusConfig } from '#/api/iot/device/modbus/config'; import { ProtocolTypeEnum } from '#/api/iot/product/product'; +import { $t } from '#/locales'; import { ModbusFrameFormatEnum, ModbusModeEnum, @@ -78,6 +79,7 @@ const [Form, formApi] = useVbenForm({ show: () => isClient.value, // Client 模式专有字段:端口 }, rules: z.number().min(1).max(65_535).optional(), + defaultValue: 502, }, { fieldName: 'slaveId', @@ -89,6 +91,7 @@ const [Form, formApi] = useVbenForm({ max: 247, }, rules: z.number().min(1, '请输入从站地址').max(247), + defaultValue: 1, }, { fieldName: 'timeout', @@ -104,6 +107,7 @@ const [Form, formApi] = useVbenForm({ show: () => isClient.value, // Client 模式专有字段:连接超时 }, rules: z.number().min(1000).optional(), + defaultValue: 3000, }, { fieldName: 'retryInterval', @@ -119,6 +123,7 @@ const [Form, formApi] = useVbenForm({ show: () => isClient.value, // Client 模式专有字段:重试间隔 }, rules: z.number().min(1000).optional(), + defaultValue: 10_000, }, { fieldName: 'mode', @@ -132,6 +137,7 @@ const [Form, formApi] = useVbenForm({ show: () => isServer.value, // Server 模式专有字段:工作模式 }, rules: 'required', + defaultValue: ModbusModeEnum.POLLING, }, { fieldName: 'frameFormat', @@ -145,6 +151,7 @@ const [Form, formApi] = useVbenForm({ show: () => isServer.value, // Server 模式专有字段:帧格式 }, rules: 'required', + defaultValue: ModbusFrameFormatEnum.MODBUS_TCP, }, { fieldName: 'status', @@ -167,16 +174,17 @@ const [Modal, modalApi] = useVbenModal({ if (!valid) { return; } - // TODO @AI:这里的处理,可以参考 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/system/user/modules/form.vue 的注释风格; modalApi.lock(); + // 提交表单 const data = (await formApi.getValues()) as IotDeviceModbusConfigApi.ModbusConfig; try { data.deviceId = deviceId.value; await saveModbusConfig(data); - message.success('保存成功'); + // 关闭并提示 await modalApi.close(); emit('success'); + message.success($t('ui.actionMessage.operationSuccess')); } finally { modalApi.unlock(); } @@ -186,34 +194,23 @@ const [Modal, modalApi] = useVbenModal({ formData.value = undefined; return; } + // 加载数据 const data = modalApi.getData<{ config?: IotDeviceModbusConfigApi.ModbusConfig; deviceId: number; protocolType: string; }>(); - if (!data) return; - - // TODO @AI:这里的处理,可以参考 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/system/user/modules/form.vue 的注释风格; + if (!data) { + return; + } deviceId.value = data.deviceId; protocolType.value = data.protocolType; - - if (data.config && data.config.id) { - // 编辑模式:加载已有配置 - formData.value = { ...data.config }; - await formApi.setValues(formData.value); - } else { - // 新增模式:设置默认值 - await formApi.setValues({ - ip: '', - port: 502, - slaveId: 1, - timeout: 3000, - retryInterval: 10_000, - mode: ModbusModeEnum.POLLING, - frameFormat: ModbusFrameFormatEnum.MODBUS_TCP, - status: 0, - }); + if (!data.config) { + return; } + // 设置到 values + formData.value = { ...data.config }; + await formApi.setValues(formData.value); }, }); diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue index 0b4648302..e0d366027 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue @@ -13,7 +13,7 @@ import { computed, h, onMounted, ref } from 'vue'; import { confirm, useVbenModal } from '@vben/common-ui'; import { DICT_TYPE } from '@vben/constants'; -import { Button, message, Tag } from 'ant-design-vue'; +import { Button, message } from 'ant-design-vue'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getModbusConfig } from '#/api/iot/device/modbus/config'; @@ -180,7 +180,7 @@ function usePointColumns(): VxeTableGridOptions['columns'] { field: 'identifier', title: '标识符', minWidth: 100, - slots: { default: 'identifier' }, + cellRender: { name: 'CellTag', props: { color: 'blue' } }, }, { field: 'functionCode', @@ -199,7 +199,7 @@ function usePointColumns(): VxeTableGridOptions['columns'] { field: 'rawDataType', title: '数据类型', minWidth: 90, - slots: { default: 'rawDataType' }, + cellRender: { name: 'CellTag' }, }, { field: 'byteOrder', title: '字节序', minWidth: 80 }, { field: 'scale', title: '缩放因子', minWidth: 80 }, @@ -314,8 +314,7 @@ onMounted(async () => { - - + - - - -