feat:【iot】modbus-tcp 协议接入 40%:优化部分表单的实现,基于 dreamy-stirring-kazoo.md

This commit is contained in:
YunaiV
2026-01-17 23:34:43 +08:00
parent 8587a549c6
commit 56a25258ee
3 changed files with 113 additions and 170 deletions

View File

@@ -39,6 +39,7 @@
<span class="text-lg font-medium">点位配置</span>
<el-button type="primary" @click="handleAddPoint">
<Icon icon="ep:plus" class="mr-1" />
<!-- TODO @AI权限需要和后端接口对齐 -->
新增点位
</el-button>
</div>
@@ -106,16 +107,16 @@
</el-table-column>
<el-table-column label="状态" align="center" prop="status" min-width="80">
<template #default="scope">
<!-- TODO @AI dict-tag -->
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'" size="small">
{{ scope.row.status === 0 ? '启用' : '禁用' }}
</el-tag>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<!-- TODO @AI权限需要和后端接口对齐 -->
<el-button link type="primary" @click="handleEditPoint(scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDeletePoint(scope.row.id)">删除</el-button>
<el-button link type="danger" @click="handleDeletePoint(scope.row.id, scope.row.name)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
@@ -149,6 +150,7 @@ import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
import { ModbusFunctionCodeOptions } from '@/views/iot/utils/constants'
import { DICT_TYPE } from '@/utils/dict'
import DeviceModbusConfigForm from './DeviceModbusConfigForm.vue'
import DeviceModbusPointForm from './DeviceModbusPointForm.vue'
@@ -163,16 +165,7 @@ const props = defineProps<{
const message = useMessage()
// ======================= 连接配置 =======================
// TODO @AI默认应该都是空的
const modbusConfig = ref<DeviceModbusConfigVO>({
deviceId: props.device.id,
ip: '',
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 1000,
status: 0 // TODO @AI使用 CommonStatus
})
const modbusConfig = ref<DeviceModbusConfigVO>({} as DeviceModbusConfigVO)
/** 获取连接配置 */
const getModbusConfig = async () => {
@@ -245,13 +238,16 @@ const handleEditPoint = (row: DeviceModbusPointVO) => {
}
/** 删除点位 */
const handleDeletePoint = async (id: number) => {
// TODO @AI最好点位的名字带上。参考别的模块
// TODO @AI参考别的注释。
await message.confirm('确定要删除点位配置吗?')
await DeviceModbusPointApi.deleteModbusPoint(id)
message.success('删除成功')
await getPointPage()
const handleDeletePoint = async (id: number, name: string) => {
try {
// 删除的二次确认
await message.delConfirm('确定要删除点位【' + name + '】吗?')
// 发起删除
await DeviceModbusPointApi.deleteModbusPoint(id)
message.success('删除成功')
// 刷新列表
await getPointPage()
} catch {}
}
/** 初始化 */

View File

@@ -11,9 +11,10 @@
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入 Modbus 服务器 IP 地址" />
</el-form-item>
<el-form-item label="请输入 Modbus 端口" prop="port">
<el-form-item label="端口" prop="port">
<el-input-number
v-model="formData.port"
placeholder="请输入端口"
:min="1"
:max="65535"
controls-position="right"
@@ -26,42 +27,36 @@
:min="1"
:max="247"
controls-position="right"
placeholder="请输入从站地址,范围 1-247"
class="!w-full"
/>
<!-- TODO @AIplacehoder 需要写下 -->
</el-form-item>
<el-form-item label="连接超时" prop="timeout">
<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"
/>
<!-- TODO @AIplacehoder 需要写下 -->
<div class="text-xs text-gray-400 mt-1">单位毫秒</div>
<!-- TODO @AI上面的毫秒可以去掉 -->
</el-form-item>
<el-form-item label="重试间隔" prop="retryInterval">
<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"
/>
<!-- TODO @AIplacehoder 需要写下 -->
<div class="text-xs text-gray-400 mt-1">单位毫秒</div>
<!-- TODO @AI上面的毫秒可以去掉 -->
</el-form-item>
<el-form-item label="状态" prop="status">
<!-- TODO @AI看看别的禁用应该是怎么样的 -->
<!-- TODO @AI参考别的模块不要使用这个 -->
<el-switch
v-model="formData.status"
:active-value="0"
:inactive-value="1"
active-text="启用"
inactive-text="禁用"
:active-value="CommonStatusEnum.ENABLE"
:inactive-value="CommonStatusEnum.DISABLE"
/>
</el-form-item>
</el-form>
@@ -74,6 +69,7 @@
<script lang="ts" setup>
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'DeviceModbusConfigForm' })
@@ -92,8 +88,6 @@ const dialogTitle = ref('编辑 Modbus 连接配置')
const formLoading = ref(false)
const formRef = ref()
/** 表单数据 */
// TODO @AI
const formData = ref<DeviceModbusConfigVO>({
deviceId: props.deviceId,
ip: '',
@@ -101,7 +95,7 @@ const formData = ref<DeviceModbusConfigVO>({
slaveId: 1,
timeout: 3000,
retryInterval: 10000,
status: 0 // TODO @AIcommonstatusenum
status: CommonStatusEnum.ENABLE
})
/** 表单校验规则 */
@@ -132,22 +126,23 @@ const resetForm = () => {
slaveId: 1,
timeout: 3000,
retryInterval: 1000,
status: 0
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
/** 提交表单 */
const submitForm = async () => {
// TODO @AI注释需要补充下参考别的模块
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await formRef.value?.validate()
formLoading.value = true
formData.value.deviceId = props.deviceId
await DeviceModbusConfigApi.saveModbusConfig(formData.value)
message.success('保存成功')
dialogVisible.value = false
emit('success')
} finally {

View File

@@ -1,6 +1,5 @@
<!-- Modbus 点位表单弹窗 -->
<template>
<!-- TODO @AIplaceholder 都提供下 -->
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
@@ -17,41 +16,33 @@
class="!w-full"
@change="handleThingModelChange"
>
<!-- TODO @AI增加 option 里的告警 -->
<el-option
v-for="item in propertyList"
:key="item.id"
:key="item.id!"
:label="`${item.name} (${item.identifier})`"
:value="item.id"
:value="item.id!"
/>
</el-select>
</el-form-item>
<el-form-item label="功能码" prop="functionCode">
<!-- TODO @AIselect -->
<el-radio-group v-model="formData.functionCode">
<el-radio
<el-select v-model="formData.functionCode" placeholder="请选择功能码" class="!w-full">
<el-option
v-for="item in ModbusFunctionCodeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
class="!mr-4"
>
{{ item.label }}
</el-radio>
</el-radio-group>
/>
</el-select>
</el-form-item>
<!-- TODO @AI不要转换直接输入 -->
<el-form-item label="寄存器地址" prop="registerAddress">
<el-input
v-model="registerAddressInput"
placeholder="请输入寄存器地址(支持十进制或十六进制如 0x0000"
@blur="handleRegisterAddressBlur"
>
<template #append>
<span class="text-gray-500">
= {{ formatRegisterAddress(formData.registerAddress) }}
</span>
</template>
</el-input>
<el-input-number
v-model="formData.registerAddress"
:min="0"
:max="65535"
controls-position="right"
placeholder="请输入寄存器地址"
class="!w-full"
/>
</el-form-item>
<el-form-item label="寄存器数量" prop="registerCount">
<el-input-number
@@ -59,6 +50,7 @@
:min="1"
:max="125"
controls-position="right"
placeholder="请输入寄存器数量"
class="!w-full"
/>
</el-form-item>
@@ -93,30 +85,25 @@
:precision="6"
:step="0.1"
controls-position="right"
placeholder="请输入缩放因子"
class="!w-full"
/>
<!-- TODO @AItip使用默认的 el-input-number 里的 -->
<div class="text-xs text-gray-400 mt-1">
读取时实际值 = 原始值 × 缩放因子写入时原始值 = 实际值 ÷ 缩放因子
</div>
</el-form-item>
<el-form-item label="轮询间隔" prop="pollInterval">
<el-form-item label="轮询间隔(ms)" prop="pollInterval">
<el-input-number
v-model="formData.pollInterval"
:min="100"
:step="1000"
controls-position="right"
placeholder="请输入轮询间隔"
class="!w-full"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<!-- TODO @AI参考下别的模块开关 -->
<el-switch
v-model="formData.status"
:active-value="0"
:inactive-value="1"
active-text="启用"
inactive-text="禁用"
:active-value="CommonStatusEnum.ENABLE"
:inactive-value="CommonStatusEnum.DISABLE"
/>
</el-form-item>
</el-form>
@@ -135,6 +122,7 @@ import {
ModbusRawDataTypeOptions,
getByteOrderOptions
} from '@/views/iot/utils/constants'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'DeviceModbusPointForm' })
@@ -149,19 +137,13 @@ const emit = defineEmits<{
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const formRef = ref()
// TODO @AI注释的风格你看看是不是有些地方要尾注释和别的模块保持一样的风格
/** 表单数据 */
// TODO @AI里面的枚举类型改成直接用枚举
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<DeviceModbusPointVO>({
deviceId: props.deviceId,
thingModelId: 0,
thingModelId: 0, // TODO @AI不要有默认值
identifier: '',
name: '',
functionCode: 3,
@@ -171,13 +153,8 @@ const formData = ref<DeviceModbusPointVO>({
rawDataType: 'INT16',
scale: 1,
pollInterval: 5000,
status: 0
})
/** 寄存器地址输入框(支持十六进制) */
const registerAddressInput = ref('0')
/** 表单校验规则 */
status: CommonStatusEnum.ENABLE
}) // TODO @AIregisterAddress、registerAddress、registerCount、rawDataType 不要有默认值;
const formRules = {
thingModelId: [{ required: true, message: '请选择物模型属性', trigger: 'change' }],
functionCode: [{ required: true, message: '请选择功能码', trigger: 'change' }],
@@ -186,10 +163,12 @@ const formRules = {
rawDataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }],
pollInterval: [{ required: true, message: '请输入轮询间隔', trigger: 'blur' }]
}
const formRef = ref() // 表单 Ref
/** 筛选属性类型的物模型 */
const propertyList = computed(() => {
return props.thingModelList.filter((item) => item.type === 1) // type=1 为属性
// TODO @AI1 使用枚举代替魔法数字
return props.thingModelList.filter((item) => item.type === 1)
})
/** 当前字节序选项(根据数据类型动态变化) */
@@ -197,53 +176,12 @@ const currentByteOrderOptions = computed(() => {
return getByteOrderOptions(formData.value.rawDataType)
})
/** 打开弹窗 */
const open = async (type: 'create' | 'update', id?: number) => {
dialogVisible.value = true
formType.value = type
// TODO @AI参考别的模块写法
dialogTitle.value = type === 'create' ? '新增 Modbus 点位' : '编辑 Modbus 点位'
resetForm()
if (type === 'update' && id) {
formLoading.value = true
try {
const data = await DeviceModbusPointApi.getModbusPoint(id)
formData.value = data
registerAddressInput.value = formatRegisterAddress(data.registerAddress)
} finally {
formLoading.value = false
}
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
deviceId: props.deviceId,
thingModelId: 0,
identifier: '',
name: '',
functionCode: 3,
registerAddress: 0,
registerCount: 1,
byteOrder: 'AB',
rawDataType: 'INT16',
scale: 1,
pollInterval: 5000,
status: 0
}
registerAddressInput.value = '0'
formRef.value?.resetFields()
}
/** 物模型属性变化 */
const handleThingModelChange = (thingModelId: number) => {
const thingModel = props.thingModelList.find((item) => item.id === thingModelId)
// TODO @AI这里有 linter 告警,可以看看。
if (thingModel) {
formData.value.identifier = thingModel.identifier
formData.value.name = thingModel.name
formData.value.identifier = thingModel.identifier!
formData.value.name = thingModel.name!
}
}
@@ -262,39 +200,33 @@ const handleRawDataTypeChange = (rawDataType: string) => {
}
}
/** 寄存器地址输入框失焦 */
const handleRegisterAddressBlur = () => {
const input = registerAddressInput.value.trim()
let address = 0
if (input.toLowerCase().startsWith('0x')) {
// 十六进制
address = parseInt(input, 16)
} else {
// 十进制
address = parseInt(input, 10)
/** 打开弹窗 */
const open = async (type: 'create' | 'update', id?: number) => {
dialogVisible.value = true
formType.value = type
// TODO @AIdialogTitle.value = t('action.' + type)
dialogTitle.value = type === 'create' ? '新增 Modbus 点位' : '编辑 Modbus 点位'
resetForm()
// 修改时,设置数据
if (type === 'update' && id) {
formLoading.value = true
try {
formData.value = await DeviceModbusPointApi.getModbusPoint(id)
} finally {
formLoading.value = false
}
}
if (isNaN(address) || address < 0) {
address = 0
}
formData.value.registerAddress = address
registerAddressInput.value = formatRegisterAddress(address)
}
/** 格式化寄存器地址为十六进制 */
const formatRegisterAddress = (address: number) => {
return '0x' + address.toString(16).toUpperCase().padStart(4, '0')
}
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await formRef.value?.validate()
formLoading.value = true
// TODO @AI这里的注释风格可以看看。
if (formType.value === 'create') {
await DeviceModbusPointApi.createModbusPoint(formData.value)
message.success('创建成功')
@@ -302,14 +234,34 @@ const submitForm = async () => {
await DeviceModbusPointApi.updateModbusPoint(formData.value)
message.success('更新成功')
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
deviceId: props.deviceId,
thingModelId: 0,
identifier: '',
name: '',
// TODO @AI3、0、1、AB/INT16 使用枚举;
functionCode: 3,
registerAddress: 0,
registerCount: 1,
byteOrder: 'AB',
rawDataType: 'INT16',
scale: 1,
pollInterval: 5000,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
/** 暴露方法 */
defineExpose({ open })
</script>