383 lines
13 KiB
Vue
383 lines
13 KiB
Vue
<!-- 产品的物模型表单(property 项) -->
|
||
<template>
|
||
<el-form-item
|
||
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
|
||
label="数据类型"
|
||
prop="property.dataType"
|
||
>
|
||
<el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
|
||
<!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) -->
|
||
<el-option
|
||
v-for="option in getDataTypeOptions2"
|
||
:key="option.value"
|
||
:label="`${option.value}(${option.label})`"
|
||
:value="option.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<!-- 数值型配置 -->
|
||
<ThingModelNumberDataSpecs
|
||
v-if="
|
||
[
|
||
IoTDataSpecsDataTypeEnum.INT,
|
||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||
IoTDataSpecsDataTypeEnum.FLOAT
|
||
].includes(property.dataType || '')
|
||
"
|
||
v-model="property.dataSpecs"
|
||
/>
|
||
<!-- 枚举型配置 -->
|
||
<ThingModelEnumDataSpecs
|
||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.ENUM"
|
||
v-model="property.dataSpecsList"
|
||
/>
|
||
<!-- 布尔型配置 -->
|
||
<el-form-item v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" label="布尔值">
|
||
<template v-for="(item, index) in property.dataSpecsList" :key="item.value">
|
||
<div class="flex items-center justify-start w-1/1 mb-5px">
|
||
<span>{{ item.value }}</span>
|
||
<span class="mx-2">-</span>
|
||
<el-form-item
|
||
:prop="`property.dataSpecsList[${index}].name`"
|
||
:rules="[
|
||
{ required: true, message: '枚举描述不能为空' },
|
||
{ validator: validateBoolName, trigger: 'blur' }
|
||
]"
|
||
class="flex-1 mb-0"
|
||
>
|
||
<el-input
|
||
v-model="item.name"
|
||
:placeholder="`如:${item.value === 0 ? '关' : '开'}`"
|
||
class="w-255px!"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
<!-- 文本型配置 -->
|
||
<el-form-item
|
||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
|
||
label="数据长度"
|
||
prop="property.dataSpecs.length"
|
||
>
|
||
<el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
|
||
<template #append>字节</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<!-- 时间型配置 -->
|
||
<el-form-item
|
||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
|
||
label="时间格式"
|
||
prop="date"
|
||
>
|
||
<el-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" />
|
||
</el-form-item>
|
||
<!-- 数组型配置-->
|
||
<ThingModelArrayDataSpecs
|
||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.ARRAY"
|
||
v-model="property.dataSpecs"
|
||
/>
|
||
<!-- Struct 型配置-->
|
||
<ThingModelStructDataSpecs
|
||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
|
||
v-model="property.dataSpecsList"
|
||
/>
|
||
<el-form-item v-if="!isStructDataSpecs && !isParams" label="读写类型" prop="property.accessMode">
|
||
<el-radio-group v-model="property.accessMode">
|
||
<el-radio
|
||
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
|
||
:key="accessMode.value"
|
||
:label="accessMode.value"
|
||
>
|
||
{{ accessMode.label }}
|
||
</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<!-- Modbus 配置区域 -->
|
||
<div v-if="isModbusProduct && !isStructDataSpecs && !isParams" class="modbus-config-section">
|
||
<el-divider content-position="left">Modbus 配置</el-divider>
|
||
<!-- 数据采集模式 -->
|
||
<el-form-item label="数据采集模式" prop="property.modbusConfig.collectionMode">
|
||
<el-radio-group v-model="property.modbusConfig.collectionMode" @change="handleCollectionModeChange">
|
||
<el-radio
|
||
v-for="mode in Object.values(IoTModbusCollectionModeEnum)"
|
||
:key="mode.value"
|
||
:label="mode.value"
|
||
>
|
||
{{ mode.label }}
|
||
</el-radio>
|
||
</el-radio-group>
|
||
|
||
</el-form-item>
|
||
<!-- 采集频率(仅在定时轮询模式下显示) -->
|
||
<el-form-item
|
||
v-if="property.modbusConfig.collectionMode === IoTModbusCollectionModeEnum.POLLING.value"
|
||
label="采集频率"
|
||
prop="property.modbusConfig.pollingInterval"
|
||
>
|
||
<el-input-number
|
||
v-model="property.modbusConfig.pollingInterval"
|
||
:min="10"
|
||
:max="86400"
|
||
placeholder="请输入采集频率(10-86400秒)"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<!-- 功能码 -->
|
||
<el-form-item label="功能码" prop="property.modbusConfig.functionCode">
|
||
<el-input-number
|
||
v-model="property.modbusConfig.functionCode"
|
||
:min="1"
|
||
:max="255"
|
||
placeholder="请输入功能码(1-255)"
|
||
@change="handleFunctionCodeChange"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<!-- 寄存器地址 -->
|
||
<el-form-item label="寄存器地址" prop="property.modbusConfig.registerAddress">
|
||
<el-input-number
|
||
v-model="property.modbusConfig.registerAddress"
|
||
:min="0"
|
||
:max="65535"
|
||
placeholder="请输入寄存器地址(0-65535)"
|
||
@change="handleRegisterAddressChange"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<!-- 寄存器数量 / 线圈数量 -->
|
||
<el-form-item label="寄存器数量" prop="property.modbusConfig.registerCount">
|
||
<el-input-number
|
||
v-model="property.modbusConfig.registerCount"
|
||
:min="1"
|
||
:max="255"
|
||
placeholder="请输入寄存器数量(1-255)"
|
||
@change="handleRegisterCountChange"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<!-- 字节序 -->
|
||
<el-form-item label="字节序" prop="property.modbusConfig.byteOrder">
|
||
<el-radio-group v-model="property.modbusConfig.byteOrder">
|
||
<el-radio
|
||
v-for="order in Object.values(IoTModbusByteOrderEnum)"
|
||
:key="order.value"
|
||
:label="order.value"
|
||
>
|
||
{{ order.label }}
|
||
</el-radio>
|
||
</el-radio-group>
|
||
|
||
</el-form-item>
|
||
<!-- 自动生成的命令(只读显示) -->
|
||
<el-form-item label="完整数据帧" v-if="property.modbusConfig?.fullFrame">
|
||
<el-input
|
||
v-model="property.modbusConfig.fullFrame"
|
||
readonly
|
||
placeholder="自动生成"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="CRC校验码" v-if="property.modbusConfig?.crcHex">
|
||
<el-input
|
||
v-model="property.modbusConfig.crcHex"
|
||
readonly
|
||
placeholder="自动生成"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { useVModel } from '@vueuse/core'
|
||
import {
|
||
ThingModelArrayDataSpecs,
|
||
ThingModelEnumDataSpecs,
|
||
ThingModelNumberDataSpecs,
|
||
ThingModelStructDataSpecs
|
||
} from './dataSpecs'
|
||
import { ThingModelApi, ThingModelProperty, validateBoolName } from '@/api/iot/thingmodel'
|
||
import { isEmpty } from '@/utils/is'
|
||
import {
|
||
getDataTypeOptions,
|
||
IoTDataSpecsDataTypeEnum,
|
||
IoTThingModelAccessModeEnum,
|
||
IoTModbusCollectionModeEnum,
|
||
IoTModbusByteOrderEnum,
|
||
IOT_PROVIDE_KEY
|
||
} from '@/views/iot/utils/constants'
|
||
import { ProductVO } from '@/api/iot/product/product'
|
||
import { inject } from 'vue'
|
||
import { useMessage } from '@/hooks/web/useMessage'
|
||
|
||
/** IoT 物模型属性 */
|
||
defineOptions({ name: 'ThingModelProperty' })
|
||
|
||
const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean; isParams?: boolean }>()
|
||
const emits = defineEmits(['update:modelValue'])
|
||
const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty>
|
||
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
|
||
const message = useMessage()
|
||
|
||
// 计算属性:是否为 Modbus 产品
|
||
const isModbusProduct = computed(() => {
|
||
if (!product?.value) return false
|
||
return product.value.protocolType === 'MODBUS_RTU' || product.value.protocolType === 'MODBUS_TCP'
|
||
})
|
||
const getDataTypeOptions2 = computed(() => {
|
||
if (!props.isStructDataSpecs) {
|
||
return getDataTypeOptions()
|
||
}
|
||
const excludedTypes = [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY]
|
||
return getDataTypeOptions().filter((item: any) => !excludedTypes.includes(item.value))
|
||
}) // 获得数据类型列表
|
||
|
||
/** 属性值的数据类型切换时初始化相关数据 */
|
||
const handleChange = (dataType: any) => {
|
||
property.value.dataSpecs = {}
|
||
property.value.dataSpecsList = []
|
||
// 不是列表型数据才设置 dataSpecs.dataType
|
||
![
|
||
IoTDataSpecsDataTypeEnum.ENUM,
|
||
IoTDataSpecsDataTypeEnum.BOOL,
|
||
IoTDataSpecsDataTypeEnum.STRUCT
|
||
].includes(dataType) && (property.value.dataSpecs.dataType = dataType)
|
||
switch (dataType) {
|
||
case IoTDataSpecsDataTypeEnum.ENUM:
|
||
property.value.dataSpecsList.push({
|
||
dataType: IoTDataSpecsDataTypeEnum.ENUM,
|
||
name: '', // 枚举项的名称
|
||
value: undefined // 枚举值
|
||
})
|
||
break
|
||
case IoTDataSpecsDataTypeEnum.BOOL:
|
||
for (let i = 0; i < 2; i++) {
|
||
property.value.dataSpecsList.push({
|
||
dataType: IoTDataSpecsDataTypeEnum.BOOL,
|
||
name: '', // 布尔值的名称
|
||
value: i // 布尔值
|
||
})
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
/** 默认选中读写 */
|
||
watch(
|
||
() => property.value.accessMode,
|
||
(val: string) => {
|
||
if (props.isStructDataSpecs || props.isParams) {
|
||
return
|
||
}
|
||
isEmpty(val) && (property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value)
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
/** 初始化 Modbus 配置 */
|
||
watch(
|
||
() => isModbusProduct.value,
|
||
(isModbus) => {
|
||
if (isModbus && !property.value.modbusConfig) {
|
||
property.value.modbusConfig = {
|
||
collectionMode: IoTModbusCollectionModeEnum.POLLING.value, // 默认为定时轮询模式
|
||
functionCode: undefined, // 功能码由用户输入,不设置默认值
|
||
registerAddress: undefined,
|
||
registerCount: 1, // 默认读取 1 个寄存器
|
||
pollingInterval: 300, // 默认采集频率 300 秒(5分钟)
|
||
fullFrame: '',
|
||
crcHex: ''
|
||
}
|
||
} else if (isModbus && property.value.modbusConfig && !property.value.modbusConfig.collectionMode) {
|
||
// 向下兼容:如果没有 collectionMode,默认设置为定时轮询
|
||
property.value.modbusConfig.collectionMode = IoTModbusCollectionModeEnum.POLLING.value
|
||
}
|
||
// 向下兼容:如果没有 registerCount,默认设置为 1
|
||
if (isModbus && property.value.modbusConfig && !property.value.modbusConfig.registerCount) {
|
||
property.value.modbusConfig.registerCount = 1
|
||
}
|
||
// 向下兼容:如果没有 pollingInterval,默认设置为 300
|
||
if (isModbus && property.value.modbusConfig && !property.value.modbusConfig.pollingInterval) {
|
||
property.value.modbusConfig.pollingInterval = 300
|
||
}
|
||
// 向下兼容:如果没有 byteOrder,默认设置为大端模式
|
||
if (isModbus && property.value.modbusConfig && !property.value.modbusConfig.byteOrder) {
|
||
property.value.modbusConfig.byteOrder = IoTModbusByteOrderEnum.BIG_ENDIAN.value
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
/** 寄存器地址变化时自动计算命令 */
|
||
const handleRegisterAddressChange = async (registerAddress: number | undefined) => {
|
||
if (!registerAddress || !product?.value?.id) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const { data } = await ThingModelApi.calculateModbusCommand({
|
||
productId: product.value.id,
|
||
registerAddress: registerAddress,
|
||
functionCode: property.value.modbusConfig?.functionCode || 3,
|
||
registerCount: property.value.modbusConfig?.registerCount || 1,
|
||
byteOrder: property.value.modbusConfig?.byteOrder || 2 // 默认小端模式
|
||
})
|
||
|
||
// 更新表单数据
|
||
if (!property.value.modbusConfig) {
|
||
property.value.modbusConfig = {}
|
||
}
|
||
property.value.modbusConfig.fullFrame = data.fullFrame
|
||
property.value.modbusConfig.crcHex = data.crcHex
|
||
|
||
message.success('Modbus 命令已自动生成')
|
||
} catch (error: any) {
|
||
console.error('生成 Modbus 命令失败', error)
|
||
message.error('生成命令失败:' + (error.message || '未知错误'))
|
||
}
|
||
}
|
||
|
||
/** 功能码变化时重新计算命令 */
|
||
const handleFunctionCodeChange = async (functionCode: number | undefined) => {
|
||
// 如果寄存器地址已设置,则重新生成命令
|
||
if (property.value.modbusConfig?.registerAddress) {
|
||
await handleRegisterAddressChange(property.value.modbusConfig.registerAddress)
|
||
}
|
||
}
|
||
|
||
/** 寄存器数量变化时重新计算命令 */
|
||
const handleRegisterCountChange = async (registerCount: number | undefined) => {
|
||
// 如果寄存器地址已设置,则重新生成命令
|
||
if (property.value.modbusConfig?.registerAddress) {
|
||
await handleRegisterAddressChange(property.value.modbusConfig.registerAddress)
|
||
}
|
||
}
|
||
|
||
/** 采集模式变化时的处理 */
|
||
const handleCollectionModeChange = (mode: number) => {
|
||
// 如果切换到定时轮询模式,且没有设置采集频率,则设置默认值
|
||
if (mode === IoTModbusCollectionModeEnum.POLLING.value) {
|
||
if (property.value.modbusConfig && !property.value.modbusConfig.pollingInterval) {
|
||
property.value.modbusConfig.pollingInterval = 300
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
:deep(.el-form-item) {
|
||
.el-form-item {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.modbus-config-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.modbus-config-section .el-divider {
|
||
margin-top: 0;
|
||
}
|
||
</style>
|