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' diff --git a/src/api/baota/baotainfo/index.ts b/src/api/baota/baotainfo/index.ts new file mode 100644 index 000000000..84d26dd92 --- /dev/null +++ b/src/api/baota/baotainfo/index.ts @@ -0,0 +1,57 @@ +import request from '@/config/axios' +import type { Dayjs } from 'dayjs'; + +/** 客户宝塔信息信息 */ +export interface Info { + id: number; // 主键ID + customerName?: string; // 客户名称 + baotaUrl?: string; // 宝塔面板URL + username?: string; // 宝塔面板账号 + password?: string; // 宝塔面板密码(建议加密存储) + rootUsername?: string; // 服务器root账号 + rootPassword?: string; // 服务器root密码(建议加密存储) + mysqlUsername?: string; // MySQL账号 + mysqlPassword?: string; // MySQL密码(建议加密存储) + redisPassword?: string; // Redis密码(建议加密存储) + publicIp?: string; // 公网IP(支持IPv4/IPv6) + remark: string; // 备注 + status?: number; // 状态(0正常 1停用) + } + +// 客户宝塔信息 API +export const InfoApi = { + // 查询客户宝塔信息分页 + getInfoPage: async (params: any) => { + return await request.get({ url: `/baota/info/page`, params }) + }, + + // 查询客户宝塔信息详情 + getInfo: async (id: number) => { + return await request.get({ url: `/baota/info/get?id=` + id }) + }, + + // 新增客户宝塔信息 + createInfo: async (data: Info) => { + return await request.post({ url: `/baota/info/create`, data }) + }, + + // 修改客户宝塔信息 + updateInfo: async (data: Info) => { + return await request.put({ url: `/baota/info/update`, data }) + }, + + // 删除客户宝塔信息 + deleteInfo: async (id: number) => { + return await request.delete({ url: `/baota/info/delete?id=` + id }) + }, + + /** 批量删除客户宝塔信息 */ + deleteInfoList: async (ids: number[]) => { + return await request.delete({ url: `/baota/info/delete-list?ids=${ids.join(',')}` }) + }, + + // 导出客户宝塔信息 Excel + exportInfo: async (params) => { + return await request.download({ url: `/baota/info/export-excel`, params }) + } +} \ No newline at end of file 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/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 31a2659a7..3bff08113 100644 --- a/src/api/iot/rule/data/sink/index.ts +++ b/src/api/iot/rule/data/sink/index.ts @@ -13,6 +13,7 @@ export interface DataSinkVO { | TcpConfig | WebSocketConfig | MqttConfig + | DatabaseConfig | RocketMQConfig | KafkaMQConfig | RabbitMQConfig @@ -113,6 +114,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, @@ -156,5 +168,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..63b17a605 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,17 @@ 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; + registerCount?: number; + byteOrder?: number; + }) => { + return await request.post({ url: `/iot/thing-model/calculate-modbus-command`, data }) } } 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/baota/baotainfo/InfoForm.vue b/src/views/baota/baotainfo/InfoForm.vue new file mode 100644 index 000000000..9d3f88dee --- /dev/null +++ b/src/views/baota/baotainfo/InfoForm.vue @@ -0,0 +1,186 @@ + + 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 @@ + + + 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/rule/data/sink/DataSinkForm.vue b/src/views/iot/rule/data/sink/DataSinkForm.vue index 2aca73f9d..f953d6d97 100644 --- a/src/views/iot/rule/data/sink/DataSinkForm.vue +++ b/src/views/iot/rule/data/sink/DataSinkForm.vue @@ -30,6 +30,10 @@ v-model="formData.config" /> + + + + + + + + + + + + + + + + +
{{ 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..475b89635 100644 --- a/src/views/iot/thingmodel/ThingModelService.vue +++ b/src/views/iot/thingmodel/ThingModelService.vue @@ -27,6 +27,56 @@ :direction="IoTThingModelParamDirectionEnum.OUTPUT" />
+ + + Modbus 配置(可选) + + +
+ 如果配置了完整帧,将直接使用该帧发送命令(推荐用于固定命令) +
+
+ + + + + + + + + + + + + + {{ order.label }} + + + +