mirror of
https://github.com/yudaocode/yudao-ui-admin-vue3.git
synced 2026-03-30 02:22:49 +00:00
Merge pull request !863 from 芋道源码/feature/iot
This commit is contained in:
31
src/api/iot/device/modbus/config/index.ts
Normal file
31
src/api/iot/device/modbus/config/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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 // 重试间隔,单位:毫秒
|
||||
mode: number // 模式
|
||||
frameFormat: 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}` })
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,8 @@ export interface ProductVO {
|
||||
status: number // 产品状态
|
||||
deviceType: number // 设备类型
|
||||
netType: number // 联网方式
|
||||
codecType: string // 数据格式(编解码器类型)
|
||||
protocolType: string // 协议类型
|
||||
serializeType: string // 序列化类型
|
||||
deviceCount: number // 设备数量
|
||||
createTime: Date // 创建时间
|
||||
}
|
||||
@@ -27,9 +28,23 @@ export enum DeviceTypeEnum {
|
||||
GATEWAY_SUB = 1, // 网关子设备
|
||||
GATEWAY = 2 // 网关设备
|
||||
}
|
||||
// IOT 数据格式(编解码器类型)枚举类
|
||||
export enum CodecTypeEnum {
|
||||
ALINK = 'Alink' // 阿里云 Alink 协议
|
||||
// IoT 协议类型枚举
|
||||
export enum ProtocolTypeEnum {
|
||||
TCP = 'tcp',
|
||||
UDP = 'udp',
|
||||
WEBSOCKET = 'websocket',
|
||||
HTTP = 'http',
|
||||
MQTT = 'mqtt',
|
||||
EMQX = 'emqx',
|
||||
COAP = 'coap',
|
||||
MODBUS_TCP_CLIENT = 'modbus_tcp_client',
|
||||
MODBUS_TCP_SERVER = 'modbus_tcp_server'
|
||||
}
|
||||
|
||||
// IoT 序列化类型枚举
|
||||
export enum SerializeTypeEnum {
|
||||
JSON = 'json',
|
||||
BINARY = 'binary'
|
||||
}
|
||||
|
||||
// IoT 产品 API
|
||||
|
||||
@@ -233,7 +233,8 @@ export enum DICT_TYPE {
|
||||
IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
|
||||
IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态
|
||||
IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
|
||||
IOT_CODEC_TYPE = 'iot_codec_type', // IOT 数据格式(编解码器类型)
|
||||
IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 协议类型
|
||||
IOT_SERIALIZE_TYPE = 'iot_serialize_type', // IOT 序列化类型
|
||||
IOT_LOCATION_TYPE = 'iot_location_type', // IOT 定位类型
|
||||
IOT_DEVICE_STATE = 'iot_device_state', // IOT 设备状态
|
||||
IOT_THING_MODEL_TYPE = 'iot_thing_model_type', // IOT 产品功能类型
|
||||
@@ -247,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 帧格式
|
||||
}
|
||||
|
||||
292
src/views/iot/device/device/detail/DeviceModbusConfig.vue
Normal file
292
src/views/iot/device/device/detail/DeviceModbusConfig.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<!-- 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" v-hasPermi="['iot:device:create']">
|
||||
编辑
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 详情展示 -->
|
||||
<el-descriptions :column="3" border direction="horizontal">
|
||||
<!-- Client 模式专有字段 -->
|
||||
<template v-if="isClient">
|
||||
<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>
|
||||
<!-- Client 模式专有字段 -->
|
||||
<template v-if="isClient">
|
||||
<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>
|
||||
<!-- Server 模式专有字段 -->
|
||||
<template v-if="isServer">
|
||||
<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>
|
||||
</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" v-hasPermi="['iot:device:create']">
|
||||
<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">
|
||||
<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">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleEditPoint(scope.row)"
|
||||
v-hasPermi="['iot:device:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeletePoint(scope.row.id, scope.row.name)"
|
||||
v-hasPermi="['iot:device:delete']"
|
||||
>
|
||||
删除
|
||||
</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"
|
||||
:protocol-type="product.protocolType"
|
||||
@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, 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'
|
||||
import { ModbusFunctionCodeOptions } from '@/views/iot/utils/constants'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
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()
|
||||
|
||||
// ======================= 连接配置 =======================
|
||||
const isClient = computed(() => props.product.protocolType === ProtocolTypeEnum.MODBUS_TCP_CLIENT) // 是否为 Client 模式
|
||||
const isServer = computed(() => props.product.protocolType === ProtocolTypeEnum.MODBUS_TCP_SERVER) // 是否为 Server 模式
|
||||
const modbusConfig = ref<DeviceModbusConfigVO>({} as DeviceModbusConfigVO)
|
||||
|
||||
/** 获取连接配置 */
|
||||
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, name: string) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm('确定要删除点位【' + name + '】吗?')
|
||||
// 发起删除
|
||||
await DeviceModbusPointApi.deleteModbusPoint(id)
|
||||
message.success('删除成功')
|
||||
// 刷新列表
|
||||
await getPointPage()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getModbusConfig()
|
||||
await getPointPage()
|
||||
})
|
||||
</script>
|
||||
205
src/views/iot/device/device/detail/DeviceModbusConfigForm.vue
Normal file
205
src/views/iot/device/device/detail/DeviceModbusConfigForm.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<!-- Modbus 连接配置弹窗 -->
|
||||
<template>
|
||||
<Dialog title="编辑 Modbus 连接配置" v-model="dialogVisible" width="600px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<!-- Client 模式专有字段:IP、端口、超时、重试 -->
|
||||
<template v-if="isClient">
|
||||
<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"
|
||||
:min="1"
|
||||
:max="247"
|
||||
controls-position="right"
|
||||
placeholder="请输入从站地址,范围 1-247"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- Client 模式专有字段:超时、重试 -->
|
||||
<template v-if="isClient">
|
||||
<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>
|
||||
<!-- Server 模式专有字段:模式、帧格式 -->
|
||||
<template v-if="isServer">
|
||||
<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
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</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'
|
||||
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<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单提交 loading 状态
|
||||
const isClient = computed(() => props.protocolType === ProtocolTypeEnum.MODBUS_TCP_CLIENT) // 是否为 Client 模式
|
||||
const isServer = computed(() => props.protocolType === ProtocolTypeEnum.MODBUS_TCP_SERVER) // 是否为 Server 模式
|
||||
const formData = ref<DeviceModbusConfigVO>({
|
||||
deviceId: props.deviceId,
|
||||
ip: '',
|
||||
port: 502,
|
||||
slaveId: 1,
|
||||
timeout: 3000,
|
||||
retryInterval: 10000,
|
||||
mode: ModbusModeEnum.POLLING,
|
||||
frameFormat: ModbusFrameFormatEnum.MODBUS_TCP,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
const formRules = computed(() => {
|
||||
const rules: Record<string, any[]> = {
|
||||
slaveId: [{ required: true, message: '请输入从站地址', trigger: 'blur' }]
|
||||
}
|
||||
if (isClient.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 (isServer.value) {
|
||||
rules.mode = [{ required: true, message: '请选择工作模式', trigger: 'change' }]
|
||||
rules.frameFormat = [{ required: true, message: '请选择帧格式', trigger: 'change' }]
|
||||
}
|
||||
return rules
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
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: 10000,
|
||||
mode: ModbusModeEnum.POLLING,
|
||||
frameFormat: ModbusFrameFormatEnum.MODBUS_TCP,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef.value) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value.deviceId = props.deviceId
|
||||
await DeviceModbusConfigApi.saveModbusConfig(formData.value)
|
||||
message.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露方法 */
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
286
src/views/iot/device/device/detail/DeviceModbusPointForm.vue
Normal file
286
src/views/iot/device/device/detail/DeviceModbusPointForm.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<!-- 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="物模型属性" prop="thingModelId">
|
||||
<el-select
|
||||
v-model="formData.thingModelId"
|
||||
placeholder="请选择物模型属性"
|
||||
filterable
|
||||
class="!w-full"
|
||||
@change="handleThingModelChange"
|
||||
>
|
||||
<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">
|
||||
<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"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="寄存器地址" prop="registerAddress">
|
||||
<el-input
|
||||
v-model.number="formData.registerAddress"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="65535"
|
||||
placeholder="请输入寄存器地址"
|
||||
class="!w-full"
|
||||
>
|
||||
<template #suffix>
|
||||
<span class="text-gray-400">{{ registerAddressHex }}</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"
|
||||
placeholder="请输入寄存器数量"
|
||||
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"
|
||||
placeholder="请输入缩放因子"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<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">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</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,
|
||||
IoTThingModelTypeEnum
|
||||
} from '@/views/iot/utils/constants'
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'DeviceModbusPointForm' })
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number
|
||||
thingModelList: ThingModelData[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
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: undefined,
|
||||
identifier: '',
|
||||
name: '',
|
||||
functionCode: undefined,
|
||||
registerAddress: undefined,
|
||||
registerCount: undefined,
|
||||
byteOrder: undefined,
|
||||
rawDataType: undefined,
|
||||
scale: 1,
|
||||
pollInterval: 5000,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
|
||||
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 formRef = ref() // 表单 Ref
|
||||
|
||||
/** 寄存器地址十六进制显示 */
|
||||
const registerAddressHex = computed(() => {
|
||||
if (formData.value.registerAddress === undefined || formData.value.registerAddress === null) {
|
||||
return ''
|
||||
}
|
||||
return '0x' + formData.value.registerAddress.toString(16).toUpperCase().padStart(4, '0')
|
||||
})
|
||||
|
||||
/** 筛选属性类型的物模型 */
|
||||
const propertyList = computed(() => {
|
||||
return props.thingModelList.filter((item) => item.type === IoTThingModelTypeEnum.PROPERTY)
|
||||
})
|
||||
|
||||
/** 当前字节序选项(根据数据类型动态变化) */
|
||||
const currentByteOrderOptions = computed(() => {
|
||||
if (!formData.value.rawDataType) {
|
||||
return []
|
||||
}
|
||||
return getByteOrderOptions(formData.value.rawDataType)
|
||||
})
|
||||
|
||||
/** 物模型属性变化 */
|
||||
const handleThingModelChange = (thingModelId: number) => {
|
||||
const thingModel = props.thingModelList.find((item) => item.id === thingModelId)
|
||||
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 open = async (type: 'create' | 'update', id?: number) => {
|
||||
dialogVisible.value = true
|
||||
formType.value = type
|
||||
dialogTitle.value = t('action.' + type)
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (type === 'update' && id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await DeviceModbusPointApi.getModbusPoint(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef.value) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
deviceId: props.deviceId,
|
||||
thingModelId: undefined,
|
||||
identifier: '',
|
||||
name: '',
|
||||
functionCode: undefined,
|
||||
registerAddress: undefined,
|
||||
registerCount: undefined,
|
||||
byteOrder: undefined,
|
||||
rawDataType: undefined,
|
||||
scale: 1,
|
||||
pollInterval: 5000,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 暴露方法 */
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -42,13 +42,29 @@
|
||||
@success="getDeviceData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
label="Modbus 配置"
|
||||
name="modbus"
|
||||
v-if="
|
||||
[ProtocolTypeEnum.MODBUS_TCP_CLIENT, ProtocolTypeEnum.MODBUS_TCP_SERVER].includes(
|
||||
product.protocolType as ProtocolTypeEnum
|
||||
)
|
||||
"
|
||||
>
|
||||
<DeviceModbusConfig
|
||||
v-if="activeTab === 'modbus'"
|
||||
:device="device"
|
||||
:product="product"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
</template>
|
||||
<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'
|
||||
@@ -56,6 +72,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'
|
||||
import DeviceDetailsSubDevice from './DeviceDetailsSubDevice.vue'
|
||||
|
||||
defineOptions({ name: 'IoTDeviceDetail' })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="ProductKey" prop="productKey">
|
||||
@@ -49,11 +49,7 @@
|
||||
label="联网方式"
|
||||
prop="netType"
|
||||
>
|
||||
<el-select
|
||||
v-model="formData.netType"
|
||||
placeholder="请选择联网方式"
|
||||
:disabled="formType === 'update'"
|
||||
>
|
||||
<el-select v-model="formData.netType" placeholder="请选择联网方式">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_NET_TYPE)"
|
||||
:key="dict.value"
|
||||
@@ -62,16 +58,36 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据格式" prop="codecType">
|
||||
<el-radio-group v-model="formData.codecType" :disabled="formType === 'update'">
|
||||
<el-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_CODEC_TYPE)"
|
||||
<el-form-item label="协议类型" prop="protocolType">
|
||||
<el-select v-model="formData.protocolType" placeholder="请选择协议类型">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="serializeType">
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型"
|
||||
placement="top"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<span>
|
||||
序列化类型
|
||||
<Icon icon="ep:question-filled" class="ml-2px" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select v-model="formData.serializeType" placeholder="请选择序列化类型">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_SERIALIZE_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="更多配置">
|
||||
@@ -109,7 +125,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ProductApi, ProductVO, CodecTypeEnum, DeviceTypeEnum } from '@/api/iot/product/product'
|
||||
import {
|
||||
ProductApi,
|
||||
ProductVO,
|
||||
ProtocolTypeEnum,
|
||||
SerializeTypeEnum,
|
||||
DeviceTypeEnum
|
||||
} from '@/api/iot/product/product'
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category'
|
||||
import { UploadImg } from '@/components/UploadFile'
|
||||
@@ -134,7 +156,8 @@ const formData = ref({
|
||||
description: undefined,
|
||||
deviceType: undefined,
|
||||
netType: undefined,
|
||||
codecType: CodecTypeEnum.ALINK,
|
||||
protocolType: ProtocolTypeEnum.MQTT,
|
||||
serializeType: SerializeTypeEnum.JSON,
|
||||
registerEnabled: false
|
||||
})
|
||||
const formRules = reactive({
|
||||
@@ -149,7 +172,8 @@ const formRules = reactive({
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
codecType: [{ required: true, message: '数据格式不能为空', trigger: 'change' }]
|
||||
protocolType: [{ required: true, message: '协议类型不能为空', trigger: 'change' }],
|
||||
serializeType: [{ required: true, message: '序列化类型不能为空', trigger: 'change' }]
|
||||
})
|
||||
const formRef = ref()
|
||||
const categoryList = ref<ProductCategoryVO[]>([]) // 产品分类列表
|
||||
@@ -209,7 +233,8 @@ const resetForm = () => {
|
||||
description: undefined,
|
||||
deviceType: undefined,
|
||||
netType: undefined,
|
||||
codecType: CodecTypeEnum.ALINK,
|
||||
protocolType: ProtocolTypeEnum.MQTT,
|
||||
serializeType: SerializeTypeEnum.JSON,
|
||||
registerEnabled: false
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDate(product.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据格式">
|
||||
<dict-tag :type="DICT_TYPE.IOT_CODEC_TYPE" :value="product.codecType" />
|
||||
<el-descriptions-item label="协议类型">
|
||||
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL_TYPE" :value="product.protocolType" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="序列化类型">
|
||||
<dict-tag :type="DICT_TYPE.IOT_SERIALIZE_TYPE" :value="product.serializeType" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="产品状态">
|
||||
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<!-- 定时触发器条件组配置组件 -->
|
||||
<template>
|
||||
<div class="space-y-16px">
|
||||
<!-- 条件组容器头部 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-blue-50 to-cyan-50 border border-blue-200 rounded-8px"
|
||||
>
|
||||
<div class="flex items-center gap-12px">
|
||||
<div class="flex items-center gap-8px text-16px font-600 text-blue-700">
|
||||
<div
|
||||
class="w-24px h-24px bg-blue-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
||||
>
|
||||
组
|
||||
</div>
|
||||
<span>附加条件组</span>
|
||||
</div>
|
||||
<el-tag size="small" type="info">定时触发时需满足以下条件</el-tag>
|
||||
<el-tag size="small" type="warning"> {{ conditionGroups?.length || 0 }} 个子条件组 </el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addConditionGroup"
|
||||
:disabled="(conditionGroups?.length || 0) >= maxGroups"
|
||||
>
|
||||
<Icon icon="ep:plus" />
|
||||
添加条件组
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 条件组列表 -->
|
||||
<div v-if="conditionGroups && conditionGroups.length > 0" class="space-y-16px">
|
||||
<div
|
||||
v-for="(group, groupIndex) in conditionGroups"
|
||||
:key="`group-${groupIndex}`"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 条件组容器 -->
|
||||
<div
|
||||
class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px"
|
||||
>
|
||||
<div class="flex items-center gap-12px">
|
||||
<div class="flex items-center gap-8px text-16px font-600 text-orange-700">
|
||||
<div
|
||||
class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
||||
>
|
||||
{{ groupIndex + 1 }}
|
||||
</div>
|
||||
<span>子条件组 {{ groupIndex + 1 }}</span>
|
||||
</div>
|
||||
<el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag>
|
||||
<el-tag size="small" type="info"> {{ group?.length || 0 }}个条件 </el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
@click="removeConditionGroup(groupIndex)"
|
||||
class="hover:bg-red-50"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
删除组
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<SubConditionGroupConfig
|
||||
:model-value="group"
|
||||
@update:model-value="(value) => updateConditionGroup(groupIndex, value)"
|
||||
:trigger-type="IotRuleSceneTriggerTypeEnum.TIMER"
|
||||
:max-conditions="maxConditionsPerGroup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 条件组间的"或"连接符 -->
|
||||
<div
|
||||
v-if="groupIndex < conditionGroups.length - 1"
|
||||
class="flex items-center justify-center py-12px"
|
||||
>
|
||||
<div class="flex items-center gap-8px">
|
||||
<!-- 连接线 -->
|
||||
<div class="w-32px h-1px bg-orange-300"></div>
|
||||
<!-- 或标签 -->
|
||||
<div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
|
||||
<span class="text-14px font-600 text-orange-600">或</span>
|
||||
</div>
|
||||
<!-- 连接线 -->
|
||||
<div class="w-32px h-1px bg-orange-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="p-24px border-2 border-dashed border-blue-200 rounded-8px text-center bg-blue-50"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-12px">
|
||||
<Icon icon="ep:plus" class="text-32px text-blue-400" />
|
||||
<div class="text-blue-600">
|
||||
<p class="text-14px font-500 mb-4px">暂无附加条件</p>
|
||||
<p class="text-12px">定时触发时将直接执行动作</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
|
||||
import type { TriggerCondition } from '@/api/iot/rule/scene'
|
||||
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
|
||||
|
||||
/** 定时触发器条件组配置组件 */
|
||||
defineOptions({ name: 'TimerConditionGroupConfig' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: TriggerCondition[][]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: TriggerCondition[][]): void
|
||||
}>()
|
||||
|
||||
const conditionGroups = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const maxGroups = 3 // 最多 3 个条件组
|
||||
const maxConditionsPerGroup = 3 // 每组最多 3 个条件
|
||||
|
||||
/** 添加条件组 */
|
||||
const addConditionGroup = async () => {
|
||||
if (!conditionGroups.value) {
|
||||
conditionGroups.value = []
|
||||
}
|
||||
// 检查是否达到最大条件组数量限制
|
||||
if (conditionGroups.value.length >= maxGroups) {
|
||||
return
|
||||
}
|
||||
// 使用 nextTick 确保响应式更新完成后再添加新的条件组
|
||||
await nextTick()
|
||||
if (conditionGroups.value) {
|
||||
conditionGroups.value.push([])
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除条件组 */
|
||||
const removeConditionGroup = (index: number) => {
|
||||
if (conditionGroups.value) {
|
||||
conditionGroups.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新条件组 */
|
||||
const updateConditionGroup = (index: number, group: TriggerCondition[]) => {
|
||||
if (conditionGroups.value) {
|
||||
conditionGroups.value[index] = group
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -90,6 +90,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 附加条件组配置 -->
|
||||
<TimerConditionGroupConfig
|
||||
:model-value="triggerItem.conditionGroups"
|
||||
@update:model-value="(value) => updateTriggerConditionGroups(index, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,8 +121,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
|
||||
import TimerConditionGroupConfig from '../configs/TimerConditionGroupConfig.vue'
|
||||
import { Crontab } from '@/components/Crontab'
|
||||
import type { Trigger } from '@/api/iot/rule/scene'
|
||||
import type { Trigger, TriggerCondition } from '@/api/iot/rule/scene'
|
||||
import {
|
||||
getTriggerTypeLabel,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
@@ -197,6 +204,15 @@ const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
|
||||
triggers.value[index].cronExpression = cronExpression
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新触发器条件组配置
|
||||
* @param index 触发器索引
|
||||
* @param conditionGroups 条件组数组
|
||||
*/
|
||||
const updateTriggerConditionGroups = (index: number, conditionGroups: TriggerCondition[][]) => {
|
||||
triggers.value[index].conditionGroups = conditionGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触发器类型变化事件
|
||||
* @param index 触发器索引
|
||||
|
||||
@@ -219,13 +219,23 @@ const thingModelTSL = ref<IotThingModelTSLResp | null>(null) // 物模型TSL数
|
||||
const propertyGroups = computed(() => {
|
||||
const groups: { label: string; options: any[] }[] = []
|
||||
|
||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.PROPERTY)
|
||||
})
|
||||
// 设备属性上报触发器、定时触发器(条件组中的设备属性条件)
|
||||
if (
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||
props.triggerType === IotRuleSceneTriggerTypeEnum.TIMER
|
||||
) {
|
||||
const propertyOptions = propertyList.value.filter(
|
||||
(property) => property.type === IoTThingModelTypeEnum.PROPERTY
|
||||
)
|
||||
if (propertyOptions.length > 0) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||
options: propertyOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 设备事件上报触发器
|
||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.EVENT,
|
||||
@@ -233,6 +243,7 @@ const propertyGroups = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// 设备服务调用触发器
|
||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
||||
|
||||
@@ -599,3 +599,83 @@ export const JSON_PARAMS_EXAMPLE_VALUES = {
|
||||
[IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] },
|
||||
DEFAULT: { display: '""', value: '' }
|
||||
} as const
|
||||
|
||||
// ========== 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 (['INT32', 'UINT32', 'FLOAT'].includes(rawDataType)) {
|
||||
return ModbusByteOrder32Options
|
||||
}
|
||||
if (rawDataType === 'DOUBLE') {
|
||||
// 64 位暂时复用 32 位字节序
|
||||
return ModbusByteOrder32Options
|
||||
}
|
||||
return ModbusByteOrder16Options
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
<dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="detailData.status" size="small" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付金额">
|
||||
<el-tag type="success" size="small">¥{{ (detailData.price / 100.0).toFixed(2) }}</el-tag>
|
||||
<el-tag type="success" size="small">¥{{ ((detailData.price || 0) / 100.0).toFixed(2) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手续费">
|
||||
<el-tag type="warning" size="small">
|
||||
¥{{ (detailData.channelFeePrice / 100.0).toFixed(2) }}
|
||||
¥{{ ((detailData.channelFeePrice || 0) / 100.0).toFixed(2) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手续费比例">
|
||||
{{ detailData.channelFeeRate.toFixed(2) }}%
|
||||
{{ (detailData.channelFeeRate || 0).toFixed(2) }}%
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付时间">
|
||||
{{ formatDate(detailData.successTime) }}
|
||||
@@ -53,7 +53,7 @@
|
||||
<el-descriptions-item label="渠道用户">{{ detailData.channelUserId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="退款金额">
|
||||
<el-tag size="mini" type="danger">
|
||||
¥{{ (detailData.refundPrice / 100.0).toFixed(2) }}
|
||||
¥{{ ((detailData.refundPrice || 0) / 100.0).toFixed(2) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="通知 URL">{{ detailData.notifyUrl }}</el-descriptions-item>
|
||||
|
||||
Reference in New Issue
Block a user