feat(iot):Modbus 支持 Master/Slave 双模式,配置表单和详情按协议类型区分展示

1. ProtocolTypeEnum 拆分:MODBUS_TCP → MODBUS_TCP_MASTER + MODBUS_TCP_SLAVE
2. Slave 模式新增 mode(工作模式)、frameFormat(帧格式)字段,使用字典管理
3. 配置表单和详情页按 Master/Slave 模式条件展示不同字段,表单校验规则动态适配
4. 新增 DICT_TYPE:IOT_MODBUS_MODE、IOT_MODBUS_FRAME_FORMAT
5. 修复设备卡片 deviceName 过长溢出问题,添加文本截断

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
YunaiV
2026-02-08 23:33:10 +08:00
parent 5f211558fb
commit 155edc41a9
8 changed files with 160 additions and 64 deletions

View File

@@ -9,6 +9,8 @@ export interface DeviceModbusConfigVO {
slaveId: number // 从站地址
timeout: number // 连接超时时间,单位:毫秒
retryInterval: number // 重试间隔,单位:毫秒
mode: number // 模式
frameFormat: number // 帧格式
status: number // 状态
}

View File

@@ -37,7 +37,8 @@ export enum ProtocolTypeEnum {
MQTT = 'mqtt',
EMQX = 'emqx',
COAP = 'coap',
MODBUS_TCP = 'modbus_tcp'
MODBUS_TCP_MASTER = 'modbus_tcp_master',
MODBUS_TCP_SLAVE = 'modbus_tcp_slave'
}
// IoT 序列化类型枚举

View File

@@ -248,5 +248,7 @@ export enum DICT_TYPE {
IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type', // IoT 告警接收类型
IOT_OTA_TASK_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA任务设备范围
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status' // IoT OTA 记录状态
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status', // IoT OTA 记录状态
IOT_MODBUS_MODE = 'iot_modbus_mode', // IoT Modbus 工作模式
IOT_MODBUS_FRAME_FORMAT = 'iot_modbus_frame_format' // IoT Modbus 帧格式
}

View File

@@ -12,21 +12,38 @@
<!-- 详情展示 -->
<el-descriptions :column="3" border direction="horizontal">
<el-descriptions-item label="IP 地址">
{{ modbusConfig.ip || '-' }}
</el-descriptions-item>
<el-descriptions-item label="端口">
{{ modbusConfig.port || '-' }}
</el-descriptions-item>
<!-- Master 模式专有字段 -->
<template v-if="isMaster">
<el-descriptions-item label="IP 地址">
{{ modbusConfig.ip || '-' }}
</el-descriptions-item>
<el-descriptions-item label="端口">
{{ modbusConfig.port || '-' }}
</el-descriptions-item>
</template>
<!-- 公共字段 -->
<el-descriptions-item label="从站地址">
{{ modbusConfig.slaveId || '-' }}
</el-descriptions-item>
<el-descriptions-item label="连接超时">
{{ modbusConfig.timeout ? `${modbusConfig.timeout} ms` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="重试间隔">
{{ modbusConfig.retryInterval ? `${modbusConfig.retryInterval} ms` : '-' }}
</el-descriptions-item>
<!-- Master 模式专有字段 -->
<template v-if="isMaster">
<el-descriptions-item label="连接超时">
{{ modbusConfig.timeout ? `${modbusConfig.timeout} ms` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="重试间隔">
{{ modbusConfig.retryInterval ? `${modbusConfig.retryInterval} ms` : '-' }}
</el-descriptions-item>
</template>
<!-- Slave 模式专有字段 -->
<template v-if="isSlave">
<el-descriptions-item label="工作模式">
<dict-tag :type="DICT_TYPE.IOT_MODBUS_MODE" :value="modbusConfig.mode" />
</el-descriptions-item>
<el-descriptions-item label="帧格式">
<dict-tag :type="DICT_TYPE.IOT_MODBUS_FRAME_FORMAT" :value="modbusConfig.frameFormat" />
</el-descriptions-item>
</template>
<!-- 公共字段 -->
<el-descriptions-item label="状态">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="modbusConfig.status" />
</el-descriptions-item>
@@ -141,7 +158,12 @@
</ContentWrap>
<!-- 连接配置弹窗 -->
<DeviceModbusConfigForm ref="configFormRef" :device-id="device.id" @success="getModbusConfig" />
<DeviceModbusConfigForm
ref="configFormRef"
:device-id="device.id"
:protocol-type="product.protocolType"
@success="getModbusConfig"
/>
<!-- 点位表单弹窗 -->
<DeviceModbusPointForm
@@ -155,7 +177,7 @@
<script lang="ts" setup>
import { DeviceVO } from '@/api/iot/device/device'
import { ProductVO } from '@/api/iot/product/product'
import { ProductVO, ProtocolTypeEnum } from '@/api/iot/product/product'
import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
@@ -175,6 +197,8 @@ const props = defineProps<{
const message = useMessage()
// ======================= 连接配置 =======================
const isMaster = computed(() => props.product.protocolType === ProtocolTypeEnum.MODBUS_TCP_MASTER) // 是否为 Master 模式
const isSlave = computed(() => props.product.protocolType === ProtocolTypeEnum.MODBUS_TCP_SLAVE) // 是否为 Slave 模式
const modbusConfig = ref<DeviceModbusConfigVO>({} as DeviceModbusConfigVO)
/** 获取连接配置 */

View File

@@ -8,19 +8,23 @@
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入 Modbus 服务器 IP 地址" />
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input-number
v-model="formData.port"
placeholder="请输入端口"
:min="1"
:max="65535"
controls-position="right"
class="!w-full"
/>
</el-form-item>
<!-- Master 模式专有字段IP端口超时重试 -->
<template v-if="isMaster">
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入 Modbus 服务器 IP 地址" />
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input-number
v-model="formData.port"
placeholder="请输入端口"
:min="1"
:max="65535"
controls-position="right"
class="!w-full"
/>
</el-form-item>
</template>
<!-- 公共字段从站地址 -->
<el-form-item label="从站地址" prop="slaveId">
<el-input-number
v-model="formData.slaveId"
@@ -31,26 +35,55 @@
class="!w-full"
/>
</el-form-item>
<el-form-item label="连接超时(ms)" prop="timeout">
<el-input-number
v-model="formData.timeout"
:min="1000"
:step="1000"
controls-position="right"
placeholder="请输入连接超时时间"
class="!w-full"
/>
</el-form-item>
<el-form-item label="重试间隔(ms)" prop="retryInterval">
<el-input-number
v-model="formData.retryInterval"
:min="1000"
:step="1000"
controls-position="right"
placeholder="请输入重试间隔"
class="!w-full"
/>
</el-form-item>
<!-- Master 模式专有字段超时重试 -->
<template v-if="isMaster">
<el-form-item label="连接超时(ms)" prop="timeout">
<el-input-number
v-model="formData.timeout"
:min="1000"
:step="1000"
controls-position="right"
placeholder="请输入连接超时时间"
class="!w-full"
/>
</el-form-item>
<el-form-item label="重试间隔(ms)" prop="retryInterval">
<el-input-number
v-model="formData.retryInterval"
:min="1000"
:step="1000"
controls-position="right"
placeholder="请输入重试间隔"
class="!w-full"
/>
</el-form-item>
</template>
<!-- Slave 模式专有字段模式帧格式 -->
<template v-if="isSlave">
<el-form-item label="工作模式" prop="mode">
<el-radio-group v-model="formData.mode">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_MODBUS_MODE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="帧格式" prop="frameFormat">
<el-radio-group v-model="formData.frameFormat">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_MODBUS_FRAME_FORMAT)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</template>
<!-- 公共字段状态 -->
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
@@ -72,13 +105,16 @@
<script lang="ts" setup>
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { ProtocolTypeEnum } from '@/api/iot/product/product'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import { ModbusModeEnum, ModbusFrameFormatEnum } from '@/views/iot/utils/constants'
defineOptions({ name: 'DeviceModbusConfigForm' })
const props = defineProps<{
deviceId: number
protocolType: string
}>()
const emit = defineEmits<{
@@ -88,6 +124,8 @@ const emit = defineEmits<{
const message = useMessage()
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单提交 loading 状态
const isMaster = computed(() => props.protocolType === ProtocolTypeEnum.MODBUS_TCP_MASTER) // 是否为 Master 模式
const isSlave = computed(() => props.protocolType === ProtocolTypeEnum.MODBUS_TCP_SLAVE) // 是否为 Slave 模式
const formData = ref<DeviceModbusConfigVO>({
deviceId: props.deviceId,
ip: '',
@@ -95,15 +133,26 @@ const formData = ref<DeviceModbusConfigVO>({
slaveId: 1,
timeout: 3000,
retryInterval: 10000,
mode: ModbusModeEnum.POLLING,
frameFormat: ModbusFrameFormatEnum.MODBUS_TCP,
status: CommonStatusEnum.ENABLE
})
const formRules = {
ip: [{ required: true, message: '请输入 IP 地址', trigger: 'blur' }],
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
slaveId: [{ required: true, message: '请输入从站地址', trigger: 'blur' }],
timeout: [{ required: true, message: '请输入连接超时时间', trigger: 'blur' }],
retryInterval: [{ required: true, message: '请输入重试间隔', trigger: 'blur' }]
}
const formRules = computed(() => {
const rules: Record<string, any[]> = {
slaveId: [{ required: true, message: '请输入从站地址', trigger: 'blur' }]
}
if (isMaster.value) {
rules.ip = [{ required: true, message: '请输入 IP 地址', trigger: 'blur' }]
rules.port = [{ required: true, message: '请输入端口', trigger: 'blur' }]
rules.timeout = [{ required: true, message: '请输入连接超时时间', trigger: 'blur' }]
rules.retryInterval = [{ required: true, message: '请输入重试间隔', trigger: 'blur' }]
}
if (isSlave.value) {
rules.mode = [{ required: true, message: '请选择工作模式', trigger: 'change' }]
rules.frameFormat = [{ required: true, message: '请选择帧格式', trigger: 'change' }]
}
return rules
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
@@ -124,7 +173,9 @@ const resetForm = () => {
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 1000,
retryInterval: 10000,
mode: ModbusModeEnum.POLLING,
frameFormat: ModbusFrameFormatEnum.MODBUS_TCP,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()

View File

@@ -45,7 +45,11 @@
<el-tab-pane
label="Modbus 配置"
name="modbus"
v-if="product.codecType === 'ModbusTcp'"
v-if="
[ProtocolTypeEnum.MODBUS_TCP_MASTER, ProtocolTypeEnum.MODBUS_TCP_SLAVE].includes(
product.protocolType as ProtocolTypeEnum
)
"
>
<DeviceModbusConfig
v-if="activeTab === 'modbus'"
@@ -60,7 +64,7 @@
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceTypeEnum, ProductApi, ProductVO, ProtocolTypeEnum } from '@/api/iot/product/product'
import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'

View File

@@ -173,7 +173,7 @@
<div class="mr-2.5 flex items-center">
<el-image :src="defaultIconUrl" class="w-[18px] h-[18px]" />
</div>
<div class="text-[16px] font-600 flex-1">{{ item.deviceName }}</div>
<div class="text-[16px] font-600 flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{{ item.deviceName }}</div>
<!-- 添加设备状态标签 -->
<div class="inline-flex items-center">
<div

View File

@@ -600,7 +600,19 @@ export const JSON_PARAMS_EXAMPLE_VALUES = {
DEFAULT: { display: '""', value: '' }
} as const
// ========== Modbus 相关常量 ==========
// ========== 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 = {
@@ -614,8 +626,8 @@ export const ModbusFunctionCodeEnum = {
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位数据' }
{ value: 3, label: '03 - 读保持寄存器 (Holding Registers)', description: '可读写 16 位数据' },
{ value: 4, label: '04 - 读输入寄存器 (Input Registers)', description: '只读 16 位数据' }
]
/** Modbus 原始数据类型枚举 */
@@ -662,7 +674,7 @@ export const getByteOrderOptions = (rawDataType: string) => {
return ModbusByteOrder32Options
}
if (rawDataType === 'DOUBLE') {
// 64位暂时复用32位字节序
// 64 位暂时复用 32 位字节序
return ModbusByteOrder32Options
}
return ModbusByteOrder16Options