From 7a196bddd5143f40375b7228916c9434ebf42439 Mon Sep 17 00:00:00 2001 From: yhl186 <9033283+yhl186@user.noreply.gitee.com> Date: Fri, 12 Dec 2025 14:06:27 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E6=88=91=E7=9C=8B=E7=9D=80=20iot=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B1=9E=E6=80=A7=E4=BF=9D=E5=AD=98=E5=92=8C=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E8=A7=84=E5=88=99=E5=8C=B9=E9=85=8D=E6=98=AF=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=E6=89=A7=E8=A1=8C=E7=9A=84=EF=BC=8C=20Redis=E4=B8=AD?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E8=BF=98=E6=B2=A1=E6=9C=89=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=8C=E6=88=91=E7=8E=B0=E5=9C=A8=E5=B7=B2?= =?UTF-8?q?=E7=BB=8F=E5=AE=9E=E7=8E=B0=E4=BA=86modbus=20rtu/tcp=E7=9A=84?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E8=BD=AE=E8=AF=A2=E5=92=8C=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=20(hex=E6=A0=BC=E5=BC=8F)=E5=92=8C=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E8=81=94=E5=8A=A8=E4=B8=8B=E5=8F=91=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=EF=BC=8C=E4=BD=86=E6=98=AF=E6=9C=89=E4=B8=AA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=A6=82=E6=9E=9C=E4=BD=BF=E7=94=A8=E8=BF=99=E4=B8=AA?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=A7=84=E5=88=99=E5=8C=B9=E9=85=8D=EF=BC=8C?= =?UTF-8?q?=E9=82=A3=E6=88=91=E5=8F=AA=E8=83=BD=E8=BD=AE=E8=AF=A2=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=89=A9=E6=A8=A1=E5=9E=8B=20=E7=84=B6=E5=90=8E?= =?UTF-8?q?=E6=88=91=E7=8E=B0=E5=9C=A8=E6=98=AF=E8=BF=99=E6=A0=B7=E5=81=9A?= =?UTF-8?q?=E7=9A=84=EF=BC=9A=E8=AE=BE=E5=A4=87=20hex=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=BD=91=E5=85=B3=E8=A7=A3=E7=A0=81=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=BD=91=E5=85=B3=E8=BD=AC=E6=8D=A2=20(=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E7=BB=84=E8=A3=85)=E2=80=94=E2=80=94=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=80=BB=E7=BA=BF=E2=80=94=E2=80=94=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E8=81=94=E5=8A=A8=20=E7=84=B6=E5=90=8E=E5=AF=B9=E4=BA=8E?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=8F=91=E9=80=81=E7=9A=84=20json=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E2=80=94=E2=80=94=E2=80=94=E2=80=94=E7=BD=91=E5=85=B3?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E2=80=94=E2=80=94=E5=9C=BA=E6=99=AF=E8=81=94?= =?UTF-8?q?=E5=8A=A8=20=E4=B8=8D=E7=9F=A5=E9=81=93=E8=BF=99=E6=A0=B7?= =?UTF-8?q?=E5=8E=BB=E5=81=9A=E6=98=AF=E5=90=A6=E5=8F=AF=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E8=BF=99=E9=83=A8=E5=88=86=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=8C=E6=B5=8B=E8=AF=95=E5=8F=AF=E8=A1=8C?= =?UTF-8?q?=EF=BC=8C=E6=9A=82=E6=97=B6=E6=B2=A1=E6=9C=89=E7=94=A8=E5=88=B0?= =?UTF-8?q?=E7=94=9F=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/iot/product/product/index.ts | 2 + src/api/iot/rule/data/sink/index.ts | 38 ++ src/api/iot/thingmodel/index.ts | 18 + src/views/iot/product/product/ProductForm.vue | 59 ++- src/views/iot/rule/data/sink/DataSinkForm.vue | 10 +- .../data/sink/config/DatabaseConfigForm.vue | 367 ++++++++++++++++++ .../sink/config/components/KeyValueEditor.vue | 43 +- src/views/iot/rule/data/sink/config/index.ts | 2 + .../iot/thingmodel/ThingModelProperty.vue | 210 +++++++++- .../iot/thingmodel/ThingModelService.vue | 64 +++ src/views/iot/utils/constants.ts | 20 + 11 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 src/views/iot/rule/data/sink/config/DatabaseConfigForm.vue diff --git a/src/api/iot/product/product/index.ts b/src/api/iot/product/product/index.ts index c9f273ebe..c2a047ca9 100644 --- a/src/api/iot/product/product/index.ts +++ b/src/api/iot/product/product/index.ts @@ -16,6 +16,8 @@ export interface ProductVO { locationType: number // 设备类型 netType: number // 联网方式 codecType: string // 数据格式(编解码器类型) + protocolType?: string // 协议类型(STANDARD, MODBUS_RTU, MODBUS_TCP) + modbusSlaveId?: number // Modbus从站ID(1-247) deviceCount: number // 设备数量 createTime: Date // 创建时间 } diff --git a/src/api/iot/rule/data/sink/index.ts b/src/api/iot/rule/data/sink/index.ts index 3e2755e0d..5a70abc29 100644 --- a/src/api/iot/rule/data/sink/index.ts +++ b/src/api/iot/rule/data/sink/index.ts @@ -11,6 +11,7 @@ export interface DataSinkVO { config?: | HttpConfig | MqttConfig + | DatabaseConfig | RocketMQConfig | KafkaMQConfig | RabbitMQConfig @@ -79,6 +80,17 @@ export interface RedisStreamMQConfig extends Config { topic: string } +/** Database 配置 */ +export interface DatabaseConfig extends Config { + url: string // 数据库连接 URL + username: string // 数据库用户名 + password: string // 数据库密码 + driverClassName: string // 数据库驱动 + tableName: string // 目标表名 + fieldMapping: Record // 字段映射 + saveMetadata: boolean // 是否保存元数据 +} + /** 数据流转目的类型 */ export const IotDataSinkTypeEnum = { HTTP: 1, @@ -122,5 +134,31 @@ export const DataSinkApi = { // 查询数据流转目的(精简)列表 getDataSinkSimpleList() { return request.get({ url: '/iot/data-sink/simple-list' }) + }, + + // 自动创建数据库表 + autoCreateTable: async (data: { + url: string + username: string + password: string + driverClassName: string + tableName: string + fieldMapping: Record + saveMetadata: boolean + }) => { + return await request.post({ url: `/iot/data-sink/database/auto-create-table`, data }) + }, + + // 同步数据库表结构 + syncTableStructure: async (data: { + url: string + username: string + password: string + driverClassName: string + tableName: string + fieldMapping: Record + saveMetadata: boolean + }) => { + return await request.post({ url: `/iot/data-sink/database/sync-table-structure`, data }) } } diff --git a/src/api/iot/thingmodel/index.ts b/src/api/iot/thingmodel/index.ts index bcf9e0707..3b93c38c1 100644 --- a/src/api/iot/thingmodel/index.ts +++ b/src/api/iot/thingmodel/index.ts @@ -78,6 +78,18 @@ export interface ThingModelProperty { description?: string dataSpecs?: ThingModelProperty dataSpecsList?: ThingModelProperty[] + modbusConfig?: ModbusConfig +} + +/** Modbus 配置 */ +export interface ModbusConfig { + collectionMode?: number // 数据采集模式:1-定时轮询,2-主动上报 + functionCode?: number // 功能码:0x03-读保持寄存器,0x04-读输入寄存器等(默认0x03) + registerAddress?: number // 寄存器地址(低位字节) + registerCount?: number // 寄存器数量 / 线圈数量(默认1) + pollingInterval?: number // 采集频率(秒) + fullFrame?: string // 完整的数据帧(自动生成的HEX字符串) + crcHex?: string // CRC 校验码(自动生成的HEX字符串) } /** 物模型事件 */ @@ -101,6 +113,7 @@ export interface ThingModelService { inputParams?: ThingModelParam[] outputParams?: ThingModelParam[] method?: string + modbusConfig?: ModbusConfig // 添加 Modbus 配置 } /** 物模型参数 */ @@ -197,6 +210,11 @@ export const ThingModelApi = { // 删除产品物模型 deleteThingModel: async (id: number) => { return await request.delete({ url: `/iot/thing-model/delete?id=` + id }) + }, + + // 计算 Modbus 命令(用于前端自动生成) + calculateModbusCommand: async (data: { productId: number; registerAddress: number; functionCode?: number }) => { + return await request.post({ url: `/iot/thing-model/calculate-modbus-command`, data }) } } diff --git a/src/views/iot/product/product/ProductForm.vue b/src/views/iot/product/product/ProductForm.vue index 5247e9258..ec3c2cb16 100644 --- a/src/views/iot/product/product/ProductForm.vue +++ b/src/views/iot/product/product/ProductForm.vue @@ -74,7 +74,7 @@ - + +
Modbus 协议自动使用 HEX 格式
+
+ + + + + + + + + +
默认值:1
@@ -132,7 +153,9 @@ const formData = ref({ deviceType: undefined, locationType: undefined, netType: undefined, - codecType: CodecTypeEnum.ALINK + codecType: CodecTypeEnum.ALINK, + protocolType: 'STANDARD', + modbusSlaveId: 1 }) const formRules = reactive({ productKey: [{ required: true, message: 'ProductKey 不能为空', trigger: 'blur' }], @@ -152,6 +175,26 @@ const formRules = reactive({ const formRef = ref() const categoryList = ref([]) // 产品分类列表 +// 计算属性:是否为 Modbus 协议 +const isModbusProtocol = computed(() => { + return formData.value.protocolType === 'MODBUS_RTU' || formData.value.protocolType === 'MODBUS_TCP' +}) + +// 处理协议类型变化 +const handleProtocolTypeChange = (value: string) => { + if (value === 'MODBUS_RTU' || value === 'MODBUS_TCP') { + // Modbus 协议自动设置数据格式为 HEX + formData.value.codecType = 'HEX' + // 设置默认从站ID + if (!formData.value.modbusSlaveId) { + formData.value.modbusSlaveId = 1 + } + } else { + // 其他协议使用 Alink 格式 + formData.value.codecType = CodecTypeEnum.ALINK + } +} + /** 打开弹窗 */ const open = async (type: string, id?: number) => { dialogVisible.value = true @@ -208,7 +251,9 @@ const resetForm = () => { deviceType: undefined, locationType: undefined, netType: undefined, - codecType: CodecTypeEnum.ALINK + codecType: CodecTypeEnum.ALINK, + protocolType: 'STANDARD', + modbusSlaveId: 1 } formRef.value?.resetFields() } @@ -218,3 +263,11 @@ const generateProductKey = () => { formData.value.productKey = generateRandomStr(16) } + + diff --git a/src/views/iot/rule/data/sink/DataSinkForm.vue b/src/views/iot/rule/data/sink/DataSinkForm.vue index a4974575e..1f0f17e53 100644 --- a/src/views/iot/rule/data/sink/DataSinkForm.vue +++ b/src/views/iot/rule/data/sink/DataSinkForm.vue @@ -25,6 +25,10 @@ + + + + + + + + + + + + + + + + +
{{ tableNameError }}
+
如果配置了字段映射,可点击“自动建表”根据字段映射自动创建数据库表
+
+ +
+ + 同步表结构 + + 根据字段映射自动添加数据库表的列 +
+ +
可选配置。如果不配置,将使用设备消息中的字段名作为数据库列名
+
+ + +
+ 启用后会自动保存设备元数据:device_id(设备ID)、tenant_id(租户ID)、report_time(上报时间) +
+
+ + + + + diff --git a/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue b/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue index d0b115cdc..49a55e4d8 100644 --- a/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue +++ b/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue @@ -1,7 +1,7 @@ diff --git a/src/views/iot/thingmodel/ThingModelService.vue b/src/views/iot/thingmodel/ThingModelService.vue index 35f7c9888..ffb9574b2 100644 --- a/src/views/iot/thingmodel/ThingModelService.vue +++ b/src/views/iot/thingmodel/ThingModelService.vue @@ -27,6 +27,52 @@ :direction="IoTThingModelParamDirectionEnum.OUTPUT" /> + + + Modbus 配置(可选) + + +
+ 如果配置了完整帧,将直接使用该帧发送命令(推荐用于固定命令) +
+
+ + +
+ 常用:05=写单线圈、06=写单寄存器、15=写多线圈、16=写多寄存器 +
+
+ + + + + +
+ 功能码05:65280(0xFF00)=闭合,0(0x0000)=断开 +
+
diff --git a/src/views/baota/baotainfo/index.vue b/src/views/baota/baotainfo/index.vue new file mode 100644 index 000000000..938da97bd --- /dev/null +++ b/src/views/baota/baotainfo/index.vue @@ -0,0 +1,397 @@ + + + From c8799a1bb5ffb48c394fdae43e353b998934c006 Mon Sep 17 00:00:00 2001 From: yhl186 <9033283+yhl186@user.noreply.gitee.com> Date: Mon, 22 Dec 2025 10:00:20 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=AE=9D=E5=A1=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 4 ++-- .env.dev | 7 ++++--- .env.local | 4 ++-- .env.prod | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.env b/.env index e1024599d..1bc528bc1 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ VITE_APP_TITLE=芋道管理系统 # 项目本地运行端口号 -VITE_PORT=80 +VITE_PORT=38080 # open 运行 npm run dev 时自动打开浏览器 VITE_OPEN=true @@ -25,4 +25,4 @@ VITE_APP_DEFAULT_LOGIN_USERNAME = admin VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123 # 百度地图 -VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD' \ No newline at end of file +VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD' diff --git a/.env.dev b/.env.dev index dc7d25024..5952e33f1 100644 --- a/.env.dev +++ b/.env.dev @@ -4,8 +4,9 @@ NODE_ENV=production VITE_DEV=true # 请求路径 -VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' - +#VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' +#VITE_BASE_URL='http://192.168.20.7:48080' +VITE_BASE_URL='http://localhost:48080' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server @@ -34,4 +35,4 @@ VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' VITE_APP_CAPTCHA_ENABLE=true # GoView域名 -VITE_GOVIEW_URL='http://127.0.0.1:3000' \ No newline at end of file +VITE_GOVIEW_URL='http://127.0.0.1:3000' diff --git a/.env.local b/.env.local index 35765584d..9b998506a 100644 --- a/.env.local +++ b/.env.local @@ -4,8 +4,8 @@ NODE_ENV=development VITE_DEV=true # 请求路径 +#VITE_BASE_URL='http://192.168.20.7:48080' VITE_BASE_URL='http://localhost:48080' - # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 VITE_UPLOAD_TYPE=server @@ -31,4 +31,4 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000' VITE_APP_CAPTCHA_ENABLE=false # GoView域名 -VITE_GOVIEW_URL='http://127.0.0.1:3000' \ No newline at end of file +VITE_GOVIEW_URL='http://127.0.0.1:3000' diff --git a/.env.prod b/.env.prod index ca7cb8e40..492e92b9a 100644 --- a/.env.prod +++ b/.env.prod @@ -5,7 +5,7 @@ VITE_DEV=false # 请求路径 VITE_BASE_URL='http://localhost:48080' - +#VITE_BASE_URL='http://192.168.20.7:48080' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server @@ -31,4 +31,4 @@ VITE_OUT_DIR=dist-prod VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' # GoView域名 -VITE_GOVIEW_URL='http://127.0.0.1:3000' \ No newline at end of file +VITE_GOVIEW_URL='http://127.0.0.1:3000' From 305b807f9856729446a75f859558503e8a93dd42 Mon Sep 17 00:00:00 2001 From: yhl186 <9033283+yhl186@user.noreply.gitee.com> Date: Mon, 22 Dec 2025 11:55:50 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E5=AE=9D=E5=A1=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.dev b/.env.dev index 5952e33f1..f383da50d 100644 --- a/.env.dev +++ b/.env.dev @@ -6,6 +6,7 @@ VITE_DEV=true # 请求路径 #VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' #VITE_BASE_URL='http://192.168.20.7:48080' +#VITE_BASE_URL='http://192.168.20.6:48080' VITE_BASE_URL='http://localhost:48080' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server From a69e27fcf92655f263431e8dfa3db9dca233349e Mon Sep 17 00:00:00 2001 From: yhl186 <9033283+yhl186@user.noreply.gitee.com> Date: Mon, 22 Dec 2025 13:22:50 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E5=AE=9D=E5=A1=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.dev b/.env.dev index f383da50d..5952e33f1 100644 --- a/.env.dev +++ b/.env.dev @@ -6,7 +6,6 @@ VITE_DEV=true # 请求路径 #VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' #VITE_BASE_URL='http://192.168.20.7:48080' -#VITE_BASE_URL='http://192.168.20.6:48080' VITE_BASE_URL='http://localhost:48080' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server From d94b5f3851c594b3b565f4c27b26d5bed1d3ac8a Mon Sep 17 00:00:00 2001 From: yhl186 <9033283+yhl186@user.noreply.gitee.com> Date: Thu, 25 Dec 2025 16:23:33 +0800 Subject: [PATCH 6/7] =?UTF-8?q?iot=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/iot/device/device/index.ts | 50 +++ src/api/iot/ota/config/index.ts | 15 + src/api/iot/ota/statistics/index.ts | 34 ++ src/utils/dict.ts | 3 +- src/views/iot/device/device/DeviceForm.vue | 4 +- .../device/components/DeviceTableSelect.vue | 4 +- .../detail/DeviceDetailsPullMessage.vue | 232 ++++++++++++++ .../detail/DeviceDetailsThingModelEvent.vue | 12 + .../DeviceDetailsThingModelProperty.vue | 13 + .../detail/DeviceDetailsThingModelService.vue | 12 + src/views/iot/device/device/detail/index.vue | 4 + src/views/iot/device/device/index.vue | 6 +- .../iot/home/components/OtaMonitorCard.vue | 298 ++++++++++++++++++ src/views/iot/home/index.vue | 12 +- src/views/iot/ota/task/OtaTaskList.vue | 59 ++++ src/views/iot/product/product/ProductForm.vue | 12 +- .../ThingModelModbusCommandDialog.vue | 206 ++++++++++++ .../iot/thingmodel/ThingModelProperty.vue | 43 ++- .../iot/thingmodel/ThingModelService.vue | 39 ++- src/views/iot/thingmodel/index.vue | 36 ++- src/views/iot/utils/constants.ts | 23 +- 21 files changed, 1064 insertions(+), 53 deletions(-) create mode 100644 src/api/iot/ota/config/index.ts create mode 100644 src/api/iot/ota/statistics/index.ts create mode 100644 src/views/iot/device/device/detail/DeviceDetailsPullMessage.vue create mode 100644 src/views/iot/home/components/OtaMonitorCard.vue create mode 100644 src/views/iot/thingmodel/ThingModelModbusCommandDialog.vue diff --git a/src/api/iot/device/device/index.ts b/src/api/iot/device/device/index.ts index 2311a9a64..3b7bf2bac 100644 --- a/src/api/iot/device/device/index.ts +++ b/src/api/iot/device/device/index.ts @@ -71,6 +71,29 @@ export interface IotDeviceMessageSendReqVO { params?: any // 请求参数 } +// IoT 设备消息拉取 Request VO +export interface IotDeviceMessagePullReqVO { + deviceId: number // 设备 ID + limit?: number // 拉取数量限制(默认 10,最大 100) +} + +// IoT 设备消息拉取 Response VO +export interface IotDeviceMessagePullRespVO { + id: number // 离线消息记录 ID + messageId: string // 消息 ID + method: string // 消息方法 + message: any // 完整的消息对象 + retryCount: number // 重试次数 + createTime: Date // 创建时间 + expireTime: Date // 过期时间 +} + +// IoT 设备消息确认 Request VO +export interface IotDeviceMessageAckReqVO { + messageId: number // 消息 ID(离线消息表的主键 ID) + deviceId: number // 设备 ID +} + // 设备 API export const DeviceApi = { // 查询设备分页 @@ -161,5 +184,32 @@ export const DeviceApi = { // 发送设备消息 sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => { return await request.post({ url: `/iot/device/message/send`, data: params }) + }, + + // 拉取待处理消息(Pull 模式) + pullDeviceMessages: async (params: IotDeviceMessagePullReqVO) => { + return await request.get({ url: `/iot/device/message/pull`, params }) + }, + + // 确认消息已处理(Pull 模式) + confirmDeviceMessage: async (data: IotDeviceMessageAckReqVO) => { + return await request.post({ url: `/iot/device/message/ack`, data }) + }, + + // 获取设备离线消息列表(管理后台) + getPullMessageList: async (deviceId: number, status?: number) => { + return await request.get({ + url: `/iot/device/message/pull-list`, + params: { deviceId, status } + }) + }, + + // 删除离线消息(管理后台) + deletePullMessage: async (messageId: number, deviceId: number) => { + return await request.delete({ + url: `/iot/device/message/pull-delete`, + params: { messageId, deviceId } + }) } } + diff --git a/src/api/iot/ota/config/index.ts b/src/api/iot/ota/config/index.ts new file mode 100644 index 000000000..9b9342b29 --- /dev/null +++ b/src/api/iot/ota/config/index.ts @@ -0,0 +1,15 @@ +import request from '@/config/axios' + +export interface OtaConfig { + allowOfflinePush: boolean // 是否允许离线推送 +} + +// 获取 OTA 配置 +export const getOtaConfig = async () => { + return await request.get({ url: `/iot/ota/config/get` }) +} + +// 更新 OTA 配置 +export const updateOtaConfig = async (data: OtaConfig) => { + return await request.put({ url: `/iot/ota/config/update`, data }) +} diff --git a/src/api/iot/ota/statistics/index.ts b/src/api/iot/ota/statistics/index.ts new file mode 100644 index 000000000..35659a89d --- /dev/null +++ b/src/api/iot/ota/statistics/index.ts @@ -0,0 +1,34 @@ +import request from '@/config/axios' + +export interface OtaTaskStatistics { + // PUSH 模式统计 + pushOnlineTotalCount: number // PUSH 在线推送总数 + pushOnlineSuccessCount: number // PUSH 在线推送成功数 + + // PULL 模式统计 + pullOfflineCount: number // PULL 离线消息总数 + pullDeliveredCount: number // PULL 已送达数量(设备已拉取) + pullTodayCount: number // 今日新增 PULL 消息数 + + // 设备统计 + totalDeviceCount: number // 总设备数 + onlineDeviceCount: number // 在线设备数 + offlineDeviceCount: number // 离线设备数 + + // 任务完成率 + totalTaskCount: number // 总任务数 + completedTaskCount: number // 已完成任务数(成功+失败) +} + +// 获取 OTA 任务统计信息 +export const getOtaTaskStatistics = async (params?: { + startTime?: Date + endTime?: Date +}) => { + return await request.get({ url: `/iot/ota/task/statistics`, params }) +} + +// 导出 API +export const OtaTaskStatisticsApi = { + getOtaTaskStatistics +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index eb2d9930b..0ef47d339 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -246,5 +246,6 @@ 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_STATUS_OPTIONS = 'iot_status_options' //消息状态 } diff --git a/src/views/iot/device/device/DeviceForm.vue b/src/views/iot/device/device/DeviceForm.vue index dfed0c63e..0492649f6 100644 --- a/src/views/iot/device/device/DeviceForm.vue +++ b/src/views/iot/device/device/DeviceForm.vue @@ -23,10 +23,10 @@ /> - + diff --git a/src/views/iot/device/device/components/DeviceTableSelect.vue b/src/views/iot/device/device/components/DeviceTableSelect.vue index 73c252da5..906381921 100644 --- a/src/views/iot/device/device/components/DeviceTableSelect.vue +++ b/src/views/iot/device/device/components/DeviceTableSelect.vue @@ -25,10 +25,10 @@ /> - + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ currentMessage?.messageId }} + + + {{ currentMessage?.method }} + + + + + + {{ currentMessage?.retryCount }} + + + {{ dateFormatter(currentMessage, '', '', currentMessage?.createTime) }} + + + {{ dateFormatter(currentMessage, '', '', currentMessage?.expireTime) }} + + + + + + + + + + + + diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue index 04f9a9f53..5178ed7e8 100644 --- a/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue @@ -131,6 +131,7 @@ const getList = async () => { if (!props.deviceId) return loading.value = true try { + queryParams.deviceId = props.deviceId const data = await DeviceApi.getDeviceMessagePairPage(queryParams) list.value = data.list total.value = data.length @@ -139,6 +140,17 @@ const getList = async () => { } } +/** 监听 deviceId 变化 */ +watch( + () => props.deviceId, + (val) => { + if (val) { + queryParams.deviceId = val + getList() + } + } +) + /** 搜索按钮操作 */ const handleQuery = () => { queryParams.pageNo = 1 diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue index 438b529d6..e63b8d112 100644 --- a/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue @@ -171,6 +171,9 @@ const queryFormRef = ref() // 搜索的表单 /** 查询列表 */ const getList = async () => { + if (!props.deviceId) { + return + } loading.value = true try { const params = { @@ -185,6 +188,16 @@ const getList = async () => { } } +/** 监听 deviceId 变化 */ +watch( + () => props.deviceId, + (val) => { + if (val) { + getList() + } + } +) + /** 前端筛选数据 */ const handleFilter = () => { if (!queryParams.keyword.trim()) { diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue index fd8456162..9d5937951 100644 --- a/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue @@ -146,6 +146,7 @@ const getList = async () => { if (!props.deviceId) return loading.value = true try { + queryParams.deviceId = props.deviceId const data = await DeviceApi.getDeviceMessagePairPage(queryParams) list.value = data.list total.value = data.length @@ -154,6 +155,17 @@ const getList = async () => { } } +/** 监听 deviceId 变化 */ +watch( + () => props.deviceId, + (val) => { + if (val) { + queryParams.deviceId = val + getList() + } + } +) + /** 搜索按钮操作 */ const handleQuery = () => { queryParams.pageNo = 1 diff --git a/src/views/iot/device/device/detail/index.vue b/src/views/iot/device/device/detail/index.vue index 3ec756b5c..33f62757f 100644 --- a/src/views/iot/device/device/detail/index.vue +++ b/src/views/iot/device/device/detail/index.vue @@ -21,6 +21,9 @@ + + + - + { if (productId) { queryParams.productId = Number(productId) } - + getList() // 获取产品列表 diff --git a/src/views/iot/home/components/OtaMonitorCard.vue b/src/views/iot/home/components/OtaMonitorCard.vue new file mode 100644 index 000000000..48924a1ce --- /dev/null +++ b/src/views/iot/home/components/OtaMonitorCard.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/src/views/iot/home/index.vue b/src/views/iot/home/index.vue index 3d60acefc..4e46a97db 100644 --- a/src/views/iot/home/index.vue +++ b/src/views/iot/home/index.vue @@ -53,14 +53,21 @@ - + + + + + + + + - + + + diff --git a/src/views/iot/thingmodel/ThingModelProperty.vue b/src/views/iot/thingmodel/ThingModelProperty.vue index 8ff965559..1a33262a4 100644 --- a/src/views/iot/thingmodel/ThingModelProperty.vue +++ b/src/views/iot/thingmodel/ThingModelProperty.vue @@ -107,7 +107,7 @@ {{ mode.label }} -
{{ getCollectionModeDesc() }}
+
-
采集频率范围:10-86400秒,默认300秒(5分钟)
@@ -134,7 +133,6 @@ @change="handleFunctionCodeChange" class="w-full" /> -
常用0x03-读保持寄存器,0x04-读输入寄存器,0x01-读线圈,0x02-读离散输入
@@ -157,7 +155,19 @@ @change="handleRegisterCountChange" class="w-full" /> -
读取的寄存器或线圈的数量,默认为 1
+
+ + + + + {{ order.label }} + + + @@ -192,6 +202,7 @@ import { IoTDataSpecsDataTypeEnum, IoTThingModelAccessModeEnum, IoTModbusCollectionModeEnum, + IoTModbusByteOrderEnum, IOT_PROVIDE_KEY } from '@/views/iot/utils/constants' import { ProductVO } from '@/api/iot/product/product' @@ -288,6 +299,10 @@ watch( 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 } ) @@ -345,16 +360,7 @@ const handleCollectionModeChange = (mode: number) => { } } -/** 获取采集模式描述 */ -const getCollectionModeDesc = () => { - const mode = property.value.modbusConfig?.collectionMode - if (mode === IoTModbusCollectionModeEnum.POLLING.value) { - return IoTModbusCollectionModeEnum.POLLING.desc - } else if (mode === IoTModbusCollectionModeEnum.ACTIVE_REPORT.value) { - return IoTModbusCollectionModeEnum.ACTIVE_REPORT.desc - } - return '' -} + diff --git a/src/views/iot/thingmodel/ThingModelService.vue b/src/views/iot/thingmodel/ThingModelService.vue index ffb9574b2..475b89635 100644 --- a/src/views/iot/thingmodel/ThingModelService.vue +++ b/src/views/iot/thingmodel/ThingModelService.vue @@ -27,7 +27,7 @@ :direction="IoTThingModelParamDirectionEnum.OUTPUT" /> - + Modbus 配置(可选) @@ -45,12 +45,9 @@ v-model="service.modbusConfig.functionCode" :min="1" :max="127" - placeholder="如:5(写单个线圈)" + placeholder="如:5" clearable /> -
- 常用:05=写单线圈、06=写单寄存器、15=写多线圈、16=写多寄存器 -
-
- 功能码05:65280(0xFF00)=闭合,0(0x0000)=断开 -
+
+ + + + + {{ order.label }} + + + @@ -82,7 +86,8 @@ import { ThingModelService } from '@/api/iot/thingmodel' import { isEmpty } from '@/utils/is' import { IoTThingModelParamDirectionEnum, - IoTThingModelServiceCallTypeEnum + IoTThingModelServiceCallTypeEnum, + IoTModbusByteOrderEnum } from '@/views/iot/utils/constants' /** IoT 物模型服务 */ @@ -100,10 +105,16 @@ if (!service.value.modbusConfig) { writeValue: undefined, registerCount: undefined, fullFrame: '', - crcHex: '' + crcHex: '', + byteOrder: IoTModbusByteOrderEnum.BIG_ENDIAN.value // 默认大端模式 } +} else if (!service.value.modbusConfig.byteOrder) { + // 向下兼容:如果没有 byteOrder,默认设置为大端模式 + service.value.modbusConfig.byteOrder = IoTModbusByteOrderEnum.BIG_ENDIAN.value } + + /** 默认选中,ASYNC 异步 */ watch( () => service.value.callType, diff --git a/src/views/iot/thingmodel/index.vue b/src/views/iot/thingmodel/index.vue index 3502bfada..96a1882a8 100644 --- a/src/views/iot/thingmodel/index.vue +++ b/src/views/iot/thingmodel/index.vue @@ -63,7 +63,7 @@ - + @@ -97,12 +106,15 @@ + +