mirror of
https://github.com/yudaocode/yudao-ui-admin-vue3.git
synced 2026-03-30 02:40:18 +00:00
feat:【iot】modbus-tcp 协议接入 20%:初步实现,基于 dynamic-forging-wigderson.md 规划
This commit is contained in:
29
src/api/iot/device/modbus/config/index.ts
Normal file
29
src/api/iot/device/modbus/config/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
/** Modbus 连接配置 VO */
|
||||
export interface DeviceModbusConfigVO {
|
||||
id?: number // 主键
|
||||
deviceId: number // 设备编号
|
||||
ip: string // Modbus 服务器 IP 地址
|
||||
port: number // Modbus 服务器端口
|
||||
slaveId: number // 从站地址
|
||||
timeout: number // 连接超时时间,单位:毫秒
|
||||
retryInterval: number // 重试间隔,单位:毫秒
|
||||
status: number // 状态
|
||||
}
|
||||
|
||||
/** Modbus 连接配置 API */
|
||||
export const DeviceModbusConfigApi = {
|
||||
/** 获取设备的 Modbus 连接配置 */
|
||||
getModbusConfig: async (deviceId: number) => {
|
||||
return await request.get<DeviceModbusConfigVO>({
|
||||
url: `/iot/device-modbus-config/get`,
|
||||
params: { deviceId }
|
||||
})
|
||||
},
|
||||
|
||||
/** 保存 Modbus 连接配置 */
|
||||
saveModbusConfig: async (data: DeviceModbusConfigVO) => {
|
||||
return await request.post({ url: `/iot/device-modbus-config/save`, data })
|
||||
}
|
||||
}
|
||||
48
src/api/iot/device/modbus/point/index.ts
Normal file
48
src/api/iot/device/modbus/point/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
/** Modbus 点位配置 VO */
|
||||
export interface DeviceModbusPointVO {
|
||||
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 点位配置 API */
|
||||
export const DeviceModbusPointApi = {
|
||||
/** 获取设备的 Modbus 点位分页 */
|
||||
getModbusPointPage: async (params: any) => {
|
||||
return await request.get({ url: `/iot/device-modbus-point/page`, params })
|
||||
},
|
||||
|
||||
/** 获取 Modbus 点位详情 */
|
||||
getModbusPoint: async (id: number) => {
|
||||
return await request.get<DeviceModbusPointVO>({
|
||||
url: `/iot/device-modbus-point/get?id=${id}`
|
||||
})
|
||||
},
|
||||
|
||||
/** 创建 Modbus 点位配置 */
|
||||
createModbusPoint: async (data: DeviceModbusPointVO) => {
|
||||
return await request.post({ url: `/iot/device-modbus-point/create`, data })
|
||||
},
|
||||
|
||||
/** 更新 Modbus 点位配置 */
|
||||
updateModbusPoint: async (data: DeviceModbusPointVO) => {
|
||||
return await request.put({ url: `/iot/device-modbus-point/update`, data })
|
||||
},
|
||||
|
||||
/** 删除 Modbus 点位配置 */
|
||||
deleteModbusPoint: async (id: number) => {
|
||||
return await request.delete({ url: `/iot/device-modbus-point/delete?id=${id}` })
|
||||
}
|
||||
}
|
||||
262
src/views/iot/device/device/detail/DeviceModbusConfig.vue
Normal file
262
src/views/iot/device/device/detail/DeviceModbusConfig.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<!-- Modbus 配置 -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- 连接配置区域 -->
|
||||
<ContentWrap>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-lg font-medium">连接配置</span>
|
||||
<el-button type="primary" @click="handleEditConfig">编辑</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 详情展示 -->
|
||||
<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>
|
||||
<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>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="modbusConfig.status === 0 ? 'success' : 'danger'">
|
||||
{{ modbusConfig.status === 0 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 点位配置区域 -->
|
||||
<ContentWrap class="mt-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-lg font-medium">点位配置</span>
|
||||
<el-button type="primary" @click="handleAddPoint">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
新增点位
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :model="queryParams" :inline="true" class="-mb-15px">
|
||||
<el-form-item label="属性名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入属性名称"
|
||||
clearable
|
||||
class="!w-200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标识符" prop="identifier">
|
||||
<el-input
|
||||
v-model="queryParams.identifier"
|
||||
placeholder="请输入标识符"
|
||||
clearable
|
||||
class="!w-200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon icon="ep:search" class="mr-5px" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon icon="ep:refresh" class="mr-5px" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 点位列表 -->
|
||||
<el-table v-loading="pointLoading" :data="pointList" :stripe="true" class="mt-4">
|
||||
<el-table-column label="属性名称" align="center" prop="name" min-width="100" />
|
||||
<el-table-column label="标识符" align="center" prop="identifier" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" type="primary">{{ scope.row.identifier }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="功能码" align="center" prop="functionCode" min-width="140">
|
||||
<template #default="scope">
|
||||
{{ formatFunctionCode(scope.row.functionCode) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="寄存器地址" align="center" prop="registerAddress" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ formatRegisterAddress(scope.row.registerAddress) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="寄存器数量" align="center" prop="registerCount" min-width="90" />
|
||||
<el-table-column label="数据类型" align="center" prop="rawDataType" min-width="90">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" type="info">{{ scope.row.rawDataType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字节序" align="center" prop="byteOrder" min-width="80" />
|
||||
<el-table-column label="缩放因子" align="center" prop="scale" min-width="80" />
|
||||
<el-table-column label="轮询间隔" align="center" prop="pollInterval" min-width="90">
|
||||
<template #default="scope"> {{ scope.row.pollInterval }} ms </template>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right" width="120">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="handleEditPoint(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDeletePoint(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getPointPage"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 连接配置弹窗 -->
|
||||
<DeviceModbusConfigForm ref="configFormRef" :device-id="device.id" @success="getModbusConfig" />
|
||||
|
||||
<!-- 点位表单弹窗 -->
|
||||
<DeviceModbusPointForm
|
||||
ref="pointFormRef"
|
||||
:device-id="device.id"
|
||||
:thing-model-list="thingModelList"
|
||||
@success="getPointPage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DeviceVO } from '@/api/iot/device/device'
|
||||
import { ProductVO } 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'
|
||||
import { ModbusFunctionCodeOptions } from '@/views/iot/utils/constants'
|
||||
import DeviceModbusConfigForm from './DeviceModbusConfigForm.vue'
|
||||
import DeviceModbusPointForm from './DeviceModbusPointForm.vue'
|
||||
|
||||
defineOptions({ name: 'DeviceModbusConfig' })
|
||||
|
||||
const props = defineProps<{
|
||||
device: DeviceVO
|
||||
product: ProductVO
|
||||
thingModelList: ThingModelData[]
|
||||
}>()
|
||||
|
||||
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 getModbusConfig = async () => {
|
||||
modbusConfig.value = await DeviceModbusConfigApi.getModbusConfig(props.device.id)
|
||||
}
|
||||
|
||||
/** 编辑连接配置 */
|
||||
const configFormRef = ref()
|
||||
const handleEditConfig = () => {
|
||||
configFormRef.value?.open(modbusConfig.value)
|
||||
}
|
||||
|
||||
// ======================= 点位配置 =======================
|
||||
const pointLoading = ref(false)
|
||||
const pointList = ref<DeviceModbusPointVO[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
deviceId: props.device.id,
|
||||
name: undefined as string | undefined,
|
||||
identifier: undefined as string | undefined
|
||||
})
|
||||
|
||||
/** 获取点位分页 */
|
||||
const getPointPage = async () => {
|
||||
pointLoading.value = true
|
||||
try {
|
||||
const data = await DeviceModbusPointApi.getModbusPointPage(queryParams)
|
||||
pointList.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
pointLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getPointPage()
|
||||
}
|
||||
|
||||
/** 重置搜索 */
|
||||
const resetQuery = () => {
|
||||
queryParams.name = undefined
|
||||
queryParams.identifier = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 格式化功能码 */
|
||||
const formatFunctionCode = (code: number) => {
|
||||
const option = ModbusFunctionCodeOptions.find((item) => item.value === code)
|
||||
return option ? option.label : `${code}`
|
||||
}
|
||||
|
||||
/** 格式化寄存器地址为十六进制 */
|
||||
const formatRegisterAddress = (address: number) => {
|
||||
return '0x' + address.toString(16).toUpperCase().padStart(4, '0')
|
||||
}
|
||||
|
||||
/** 新增点位 */
|
||||
const pointFormRef = ref()
|
||||
const handleAddPoint = () => {
|
||||
pointFormRef.value?.open('create')
|
||||
}
|
||||
|
||||
/** 编辑点位 */
|
||||
const handleEditPoint = (row: DeviceModbusPointVO) => {
|
||||
pointFormRef.value?.open('update', row.id)
|
||||
}
|
||||
|
||||
/** 删除点位 */
|
||||
const handleDeletePoint = async (id: number) => {
|
||||
// TODO @AI:最好点位的名字带上。参考别的模块;
|
||||
// TODO @AI:参考别的注释。
|
||||
await message.confirm('确定要删除该点位配置吗?')
|
||||
await DeviceModbusPointApi.deleteModbusPoint(id)
|
||||
message.success('删除成功')
|
||||
await getPointPage()
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getModbusConfig()
|
||||
await getPointPage()
|
||||
})
|
||||
</script>
|
||||
160
src/views/iot/device/device/detail/DeviceModbusConfigForm.vue
Normal file
160
src/views/iot/device/device/detail/DeviceModbusConfigForm.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<!-- Modbus 连接配置弹窗 -->
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
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="请输入 Modbus 端口" prop="port">
|
||||
<el-input-number
|
||||
v-model="formData.port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="从站地址" prop="slaveId">
|
||||
<el-input-number
|
||||
v-model="formData.slaveId"
|
||||
:min="1"
|
||||
:max="247"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
<!-- TODO @AI:placehoder 需要写下 -->
|
||||
</el-form-item>
|
||||
<el-form-item label="连接超时" prop="timeout">
|
||||
<el-input-number
|
||||
v-model="formData.timeout"
|
||||
:min="1000"
|
||||
:step="1000"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
<!-- TODO @AI:placehoder 需要写下 -->
|
||||
<div class="text-xs text-gray-400 mt-1">单位:毫秒</div>
|
||||
<!-- TODO @AI:上面的毫秒,可以去掉 -->
|
||||
</el-form-item>
|
||||
<el-form-item label="重试间隔" prop="retryInterval">
|
||||
<el-input-number
|
||||
v-model="formData.retryInterval"
|
||||
:min="1000"
|
||||
:step="1000"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
<!-- TODO @AI:placehoder 需要写下 -->
|
||||
<div class="text-xs text-gray-400 mt-1">单位:毫秒</div>
|
||||
<!-- TODO @AI:上面的毫秒,可以去掉 -->
|
||||
</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="禁用"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
|
||||
|
||||
defineOptions({ name: 'DeviceModbusConfigForm' })
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('编辑 Modbus 连接配置')
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
/** 表单数据 */
|
||||
// TODO @AI;
|
||||
const formData = ref<DeviceModbusConfigVO>({
|
||||
deviceId: props.deviceId,
|
||||
ip: '',
|
||||
port: 502,
|
||||
slaveId: 1,
|
||||
timeout: 3000,
|
||||
retryInterval: 10000,
|
||||
status: 0 // TODO @AI:commonstatusenum;
|
||||
})
|
||||
|
||||
/** 表单校验规则 */
|
||||
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 open = async (data?: DeviceModbusConfigVO) => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
// 编辑模式
|
||||
if (data && data.id) {
|
||||
formData.value = { ...data }
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
deviceId: props.deviceId,
|
||||
ip: '',
|
||||
port: 502,
|
||||
slaveId: 1,
|
||||
timeout: 3000,
|
||||
retryInterval: 1000,
|
||||
status: 0
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = async () => {
|
||||
// TODO @AI:注释,需要补充下;参考别的模块
|
||||
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 {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露方法 */
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
315
src/views/iot/device/device/detail/DeviceModbusPointForm.vue
Normal file
315
src/views/iot/device/device/detail/DeviceModbusPointForm.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<!-- Modbus 点位表单弹窗 -->
|
||||
<template>
|
||||
<!-- TODO @AI:placeholder 都提供下 -->
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="物模型属性" prop="thingModelId">
|
||||
<el-select
|
||||
v-model="formData.thingModelId"
|
||||
placeholder="请选择物模型属性"
|
||||
filterable
|
||||
class="!w-full"
|
||||
@change="handleThingModelChange"
|
||||
>
|
||||
<!-- TODO @AI:增加 option 里的告警; -->
|
||||
<el-option
|
||||
v-for="item in propertyList"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (${item.identifier})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="功能码" prop="functionCode">
|
||||
<!-- TODO @AI:select -->
|
||||
<el-radio-group v-model="formData.functionCode">
|
||||
<el-radio
|
||||
v-for="item in ModbusFunctionCodeOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="!mr-4"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</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-form-item>
|
||||
<el-form-item label="寄存器数量" prop="registerCount">
|
||||
<el-input-number
|
||||
v-model="formData.registerCount"
|
||||
:min="1"
|
||||
:max="125"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="原始数据类型" prop="rawDataType">
|
||||
<el-select
|
||||
v-model="formData.rawDataType"
|
||||
placeholder="请选择数据类型"
|
||||
class="!w-full"
|
||||
@change="handleRawDataTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in ModbusRawDataTypeOptions"
|
||||
:key="item.value"
|
||||
:label="`${item.label} - ${item.description}`"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字节序" prop="byteOrder">
|
||||
<el-select v-model="formData.byteOrder" placeholder="请选择字节序" class="!w-full">
|
||||
<el-option
|
||||
v-for="item in currentByteOrderOptions"
|
||||
:key="item.value"
|
||||
:label="`${item.label} - ${item.description}`"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="缩放因子" prop="scale">
|
||||
<el-input-number
|
||||
v-model="formData.scale"
|
||||
:precision="6"
|
||||
:step="0.1"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
<!-- TODO @AI:tip,使用默认的 el-input-number 里的 -->
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
读取时:实际值 = 原始值 × 缩放因子;写入时:原始值 = 实际值 ÷ 缩放因子
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="轮询间隔" prop="pollInterval">
|
||||
<el-input-number
|
||||
v-model="formData.pollInterval"
|
||||
:min="100"
|
||||
:step="1000"
|
||||
controls-position="right"
|
||||
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="禁用"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ThingModelData } from '@/api/iot/thingmodel'
|
||||
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
|
||||
import {
|
||||
ModbusFunctionCodeOptions,
|
||||
ModbusRawDataTypeOptions,
|
||||
getByteOrderOptions
|
||||
} from '@/views/iot/utils/constants'
|
||||
|
||||
defineOptions({ name: 'DeviceModbusPointForm' })
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number
|
||||
thingModelList: ThingModelData[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
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 formData = ref<DeviceModbusPointVO>({
|
||||
deviceId: props.deviceId,
|
||||
thingModelId: 0,
|
||||
identifier: '',
|
||||
name: '',
|
||||
functionCode: 3,
|
||||
registerAddress: 0,
|
||||
registerCount: 1,
|
||||
byteOrder: 'AB',
|
||||
rawDataType: 'INT16',
|
||||
scale: 1,
|
||||
pollInterval: 5000,
|
||||
status: 0
|
||||
})
|
||||
|
||||
/** 寄存器地址输入框(支持十六进制) */
|
||||
const registerAddressInput = ref('0')
|
||||
|
||||
/** 表单校验规则 */
|
||||
const formRules = {
|
||||
thingModelId: [{ required: true, message: '请选择物模型属性', trigger: 'change' }],
|
||||
functionCode: [{ required: true, message: '请选择功能码', trigger: 'change' }],
|
||||
registerAddress: [{ required: true, message: '请输入寄存器地址', trigger: 'blur' }],
|
||||
registerCount: [{ required: true, message: '请输入寄存器数量', trigger: 'blur' }],
|
||||
rawDataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }],
|
||||
pollInterval: [{ required: true, message: '请输入轮询间隔', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
/** 筛选属性类型的物模型 */
|
||||
const propertyList = computed(() => {
|
||||
return props.thingModelList.filter((item) => item.type === 1) // type=1 为属性
|
||||
})
|
||||
|
||||
/** 当前字节序选项(根据数据类型动态变化) */
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/** 数据类型变化 */
|
||||
const handleRawDataTypeChange = (rawDataType: string) => {
|
||||
// 根据数据类型自动设置寄存器数量
|
||||
const option = ModbusRawDataTypeOptions.find((item) => item.value === rawDataType)
|
||||
if (option && option.registerCount > 0) {
|
||||
formData.value.registerCount = option.registerCount
|
||||
}
|
||||
|
||||
// 重置字节序为第一个选项
|
||||
const byteOrderOptions = getByteOrderOptions(rawDataType)
|
||||
if (byteOrderOptions.length > 0) {
|
||||
formData.value.byteOrder = byteOrderOptions[0].value
|
||||
}
|
||||
}
|
||||
|
||||
/** 寄存器地址输入框失焦 */
|
||||
const handleRegisterAddressBlur = () => {
|
||||
const input = registerAddressInput.value.trim()
|
||||
let address = 0
|
||||
|
||||
if (input.toLowerCase().startsWith('0x')) {
|
||||
// 十六进制
|
||||
address = parseInt(input, 16)
|
||||
} else {
|
||||
// 十进制
|
||||
address = parseInt(input, 10)
|
||||
}
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
formLoading.value = true
|
||||
|
||||
// TODO @AI:这里的注释风格,可以看看。
|
||||
if (formType.value === 'create') {
|
||||
await DeviceModbusPointApi.createModbusPoint(formData.value)
|
||||
message.success('创建成功')
|
||||
} else {
|
||||
await DeviceModbusPointApi.updateModbusPoint(formData.value)
|
||||
message.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露方法 */
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -36,6 +36,18 @@
|
||||
@success="getDeviceData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
label="Modbus 配置"
|
||||
name="modbus"
|
||||
v-if="product.codecType === 'ModbusTcp'"
|
||||
>
|
||||
<DeviceModbusConfig
|
||||
v-if="activeTab === 'modbus'"
|
||||
:device="device"
|
||||
:product="product"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
</template>
|
||||
@@ -50,6 +62,7 @@ import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
|
||||
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
|
||||
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
|
||||
import DeviceDetailConfig from './DeviceDetailConfig.vue'
|
||||
import DeviceModbusConfig from './DeviceModbusConfig.vue'
|
||||
|
||||
defineOptions({ name: 'IoTDeviceDetail' })
|
||||
|
||||
|
||||
@@ -541,3 +541,71 @@ export const JSON_PARAMS_EXAMPLE_VALUES = {
|
||||
[IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] },
|
||||
DEFAULT: { display: '""', value: '' }
|
||||
} as const
|
||||
|
||||
// ========== Modbus 相关常量 ==========
|
||||
|
||||
/** 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 (['INT32', 'UINT32', 'FLOAT'].includes(rawDataType)) {
|
||||
return ModbusByteOrder32Options
|
||||
}
|
||||
if (rawDataType === 'DOUBLE') {
|
||||
// 64位暂时复用32位字节序
|
||||
return ModbusByteOrder32Options
|
||||
}
|
||||
return ModbusByteOrder16Options
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user