diff --git a/.env b/.env index 79110220..0f9c97e2 100644 --- a/.env +++ b/.env @@ -31,4 +31,7 @@ VITE_APP_API_ENCRYPT_ALGORITHM = AES VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395 VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883 # VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB -# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== \ No newline at end of file +# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== + +# 百度地图 +VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD' \ No newline at end of file diff --git a/package.json b/package.json index b0aea4c4..949738db 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", "jsencrypt": "^3.3.2", + "jsoneditor": "^10.1.3", "lodash-es": "^4.17.21", "markdown-it": "^14.1.0", "markmap-common": "^0.16.0", @@ -67,7 +68,6 @@ "sortablejs": "^1.15.3", "steady-xml": "^0.1.0", "url": "^0.11.3", - "v3-jsoneditor": "^0.0.6", "video.js": "^7.21.5", "vue": "3.5.12", "vue-dompurify-html": "^4.1.4", @@ -85,6 +85,7 @@ "@iconify/json": "^2.2.187", "@intlify/unplugin-vue-i18n": "^2.0.0", "@purge-icons/generated": "^0.9.0", + "@types/jsoneditor": "^9.9.5", "@types/lodash-es": "^4.17.12", "@types/node": "^20.11.21", "@types/nprogress": "^0.2.3", diff --git a/src/api/iot/alert/config/index.ts b/src/api/iot/alert/config/index.ts new file mode 100644 index 00000000..e3ddc2a5 --- /dev/null +++ b/src/api/iot/alert/config/index.ts @@ -0,0 +1,46 @@ +import request from '@/config/axios' + +/** IoT 告警配置信息 */ +export interface AlertConfig { + id: number // 配置编号 + name?: string // 配置名称 + description: string // 配置描述 + level?: number // 告警级别 + status?: number // 配置状态 + sceneRuleIds: string // 关联的场景联动规则编号数组 + receiveUserIds: string // 接收的用户编号数组 + receiveTypes: string // 接收的类型数组 +} + +// IoT 告警配置 API +export const AlertConfigApi = { + // 查询告警配置分页 + getAlertConfigPage: async (params: any) => { + return await request.get({ url: `/iot/alert-config/page`, params }) + }, + + // 查询告警配置详情 + getAlertConfig: async (id: number) => { + return await request.get({ url: `/iot/alert-config/get?id=` + id }) + }, + + // 新增告警配置 + createAlertConfig: async (data: AlertConfig) => { + return await request.post({ url: `/iot/alert-config/create`, data }) + }, + + // 修改告警配置 + updateAlertConfig: async (data: AlertConfig) => { + return await request.put({ url: `/iot/alert-config/update`, data }) + }, + + // 删除告警配置 + deleteAlertConfig: async (id: number) => { + return await request.delete({ url: `/iot/alert-config/delete?id=` + id }) + }, + + // 获取告警配置简单列表 + getSimpleAlertConfigList: async () => { + return await request.get({ url: `/iot/alert-config/simple-list` }) + } +} diff --git a/src/api/iot/alert/record/index.ts b/src/api/iot/alert/record/index.ts new file mode 100644 index 00000000..b124a9c3 --- /dev/null +++ b/src/api/iot/alert/record/index.ts @@ -0,0 +1,35 @@ +import request from '@/config/axios' + +/** IoT 告警记录信息 */ +export interface AlertRecord { + id: number // 记录编号 + configId: number // 告警配置编号 + configName: string // 告警名称 + configLevel: number // 告警级别 + productId: number // 产品编号 + deviceId: number // 设备编号 + deviceMessage: any // 触发的设备消息 + processStatus?: boolean // 是否处理 + processRemark: string // 处理结果(备注) +} + +// IoT 告警记录 API +export const AlertRecordApi = { + // 查询告警记录分页 + getAlertRecordPage: async (params: any) => { + return await request.get({ url: `/iot/alert-record/page`, params }) + }, + + // 查询告警记录详情 + getAlertRecord: async (id: number) => { + return await request.get({ url: `/iot/alert-record/get?id=` + id }) + }, + + // 处理告警记录 + processAlertRecord: async (id: number, processRemark: string) => { + return await request.put({ + url: `/iot/alert-record/process`, + data: { id, processRemark } + }) + } +} diff --git a/src/api/iot/device/device/index.ts b/src/api/iot/device/device/index.ts index 252ea433..2311a9a6 100644 --- a/src/api/iot/device/device/index.ts +++ b/src/api/iot/device/device/index.ts @@ -3,7 +3,6 @@ import request from '@/config/axios' // IoT 设备 VO export interface DeviceVO { id: number // 设备 ID,主键,自增 - deviceKey: string // 设备唯一标识符 deviceName: string // 设备名称 productId: number // 产品编号 productKey: string // 产品标识 @@ -22,8 +21,9 @@ export interface DeviceVO { mqttUsername: string // MQTT 用户名 mqttPassword: string // MQTT 密码 authType: string // 认证类型 - latitude: number // 设备位置的纬度 - longitude: number // 设备位置的经度 + locationType: number // 定位类型 + latitude?: number // 设备位置的纬度 + longitude?: number // 设备位置的经度 areaId: number // 地区编码 address: string // 设备详细地址 serialNumber: string // 设备序列号 @@ -31,25 +31,25 @@ export interface DeviceVO { groupIds?: number[] // 添加分组 ID } -// IoT 设备数据 VO -export interface DeviceDataVO { - deviceId: number // 设备编号 - thinkModelFunctionId: number // 物模型编号 - productKey: string // 产品标识 - deviceName: string // 设备名称 +// IoT 设备属性详细 VO +export interface IotDevicePropertyDetailRespVO { identifier: string // 属性标识符 + value: string // 最新值 + updateTime: Date // 更新时间 name: string // 属性名称 dataType: string // 数据类型 - updateTime: Date // 更新时间 + dataSpecs: any // 数据定义 + dataSpecsList: any[] // 数据定义列表 +} + +// IoT 设备属性 VO +export interface IotDevicePropertyRespVO { + identifier: string // 属性标识符 value: string // 最新值 + updateTime: Date // 更新时间 } -// IoT 设备数据 VO -export interface DeviceHistoryDataVO { - time: number // 时间 - data: string // 数据 -} - +// TODO @芋艿:调整到 constants // IoT 设备状态枚举 export enum DeviceStateEnum { INACTIVE = 0, // 未激活 @@ -57,27 +57,18 @@ export enum DeviceStateEnum { OFFLINE = 2 // 离线 } -// IoT 设备上行 Request VO -export interface IotDeviceUpstreamReqVO { - id: number // 设备编号 - type: string // 消息类型 - identifier: string // 标识符 - data: any // 请求参数 +// 设备认证参数 VO +export interface IotDeviceAuthInfoVO { + clientId: string // 客户端 ID + username: string // 用户名 + password: string // 密码 } -// IoT 设备下行 Request VO -export interface IotDeviceDownstreamReqVO { - id: number // 设备编号 - type: string // 消息类型 - identifier: string // 标识符 - data: any // 请求参数 -} - -// MQTT 连接参数 VO -export interface MqttConnectionParamsVO { - mqttClientId: string // MQTT 客户端 ID - mqttUsername: string // MQTT 用户名 - mqttPassword: string // MQTT 密码 +// IoT 设备发送消息 Request VO +export interface IotDeviceMessageSendReqVO { + deviceId: number // 设备编号 + method: string // 请求方法 + params?: any // 请求参数 } // 设备 API @@ -128,8 +119,13 @@ export const DeviceApi = { }, // 获取设备的精简信息列表 - getSimpleDeviceList: async (deviceType?: number) => { - return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } }) + getSimpleDeviceList: async (deviceType?: number, productId?: number) => { + return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType, productId } }) + }, + + // 根据产品编号,获取设备的精简信息列表 + getDeviceListByProductId: async (productId: number) => { + return await request.get({ url: `/iot/device/simple-list?`, params: { productId } }) }, // 获取导入模板 @@ -137,33 +133,33 @@ export const DeviceApi = { return await request.download({ url: `/iot/device/get-import-template` }) }, - // 设备上行 - upstreamDevice: async (data: IotDeviceUpstreamReqVO) => { - return await request.post({ url: `/iot/device/upstream`, data }) - }, - - // 设备下行 - downstreamDevice: async (data: IotDeviceDownstreamReqVO) => { - return await request.post({ url: `/iot/device/downstream`, data }) - }, - // 获取设备属性最新数据 getLatestDeviceProperties: async (params: any) => { - return await request.get({ url: `/iot/device/property/latest`, params }) + return await request.get({ url: `/iot/device/property/get-latest`, params }) }, // 获取设备属性历史数据 - getHistoryDevicePropertyPage: async (params: any) => { - return await request.get({ url: `/iot/device/property/history-page`, params }) + getHistoryDevicePropertyList: async (params: any) => { + return await request.get({ url: `/iot/device/property/history-list`, params }) }, - // 查询设备日志分页 - getDeviceLogPage: async (params: any) => { - return await request.get({ url: `/iot/device/log/page`, params }) + // 获取设备认证信息 + getDeviceAuthInfo: async (id: number) => { + return await request.get({ url: `/iot/device/get-auth-info`, params: { id } }) }, - // 获取设备MQTT连接参数 - getMqttConnectionParams: async (deviceId: number) => { - return await request.get({ url: `/iot/device/mqtt-connection-params`, params: { deviceId } }) + // 查询设备消息分页 + getDeviceMessagePage: async (params: any) => { + return await request.get({ url: `/iot/device/message/page`, params }) + }, + + // 查询设备消息配对分页 + getDeviceMessagePairPage: async (params: any) => { + return await request.get({ url: `/iot/device/message/pair-page`, params }) + }, + + // 发送设备消息 + sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => { + return await request.post({ url: `/iot/device/message/send`, data: params }) } } diff --git a/src/api/iot/ota/firmware/index.ts b/src/api/iot/ota/firmware/index.ts new file mode 100644 index 00000000..97e6d059 --- /dev/null +++ b/src/api/iot/ota/firmware/index.ts @@ -0,0 +1,44 @@ +import request from '@/config/axios' + +/** IoT OTA 固件信息 */ +export interface IoTOtaFirmware { + id?: number // 固件编号 + name?: string // 固件名称 + description?: string // 固件描述 + version?: string // 版本号 + productId?: number // 产品编号 + productName?: string // 产品名称 + fileUrl?: string // 固件文件 URL + fileSize?: number // 固件文件大小 + fileDigestAlgorithm?: string // 固件文件签名算法 + fileDigestValue?: string // 固件文件签名结果 + createTime?: Date // 创建时间 +} + +// IoT OTA 固件 API +export const IoTOtaFirmwareApi = { + // 查询 OTA 固件分页 + getOtaFirmwarePage: async (params: any) => { + return await request.get({ url: `/iot/ota/firmware/page`, params }) + }, + + // 查询 OTA 固件详情 + getOtaFirmware: async (id: number) => { + return await request.get({ url: `/iot/ota/firmware/get?id=` + id }) + }, + + // 新增 OTA 固件 + createOtaFirmware: async (data: IoTOtaFirmware) => { + return await request.post({ url: `/iot/ota/firmware/create`, data }) + }, + + // 修改 OTA 固件 + updateOtaFirmware: async (data: IoTOtaFirmware) => { + return await request.put({ url: `/iot/ota/firmware/update`, data }) + }, + + // 删除 OTA 固件 + deleteOtaFirmware: async (id: number) => { + return await request.delete({ url: `/iot/ota/firmware/delete?id=` + id }) + } +} diff --git a/src/api/iot/ota/task/index.ts b/src/api/iot/ota/task/index.ts new file mode 100644 index 00000000..454405c5 --- /dev/null +++ b/src/api/iot/ota/task/index.ts @@ -0,0 +1,38 @@ +import request from '@/config/axios' + +/** IoT OTA 任务信息 */ +export interface OtaTask { + id?: number // 任务编号 + name: string // 任务名称 + description?: string // 任务描述 + firmwareId?: number // 固件编号 + status: number // 任务状态 + deviceScope?: number // 升级范围 + deviceIds?: number[] // 指定设备ID列表(当升级范围为指定设备时使用) + deviceTotalCount?: number // 设备总共数量 + deviceSuccessCount?: number // 设备成功数量 + createTime?: Date // 创建时间 +} + +// IoT OTA 任务 API +export const IoTOtaTaskApi = { + // 查询 OTA 升级任务分页 + getOtaTaskPage: async (params: any) => { + return await request.get({ url: `/iot/ota/task/page`, params }) + }, + + // 查询 OTA 升级任务详情 + getOtaTask: async (id: number) => { + return await request.get({ url: `/iot/ota/task/get?id=` + id }) + }, + + // 创建 OTA 升级任务 + createOtaTask: async (data: OtaTask) => { + return await request.post({ url: `/iot/ota/task/create`, data }) + }, + + // 取消 OTA 升级任务 + cancelOtaTask: async (id: number) => { + return await request.post({ url: `/iot/ota/task/cancel?id=` + id }) + } +} diff --git a/src/api/iot/ota/task/record/index.ts b/src/api/iot/ota/task/record/index.ts new file mode 100644 index 00000000..aedc0b93 --- /dev/null +++ b/src/api/iot/ota/task/record/index.ts @@ -0,0 +1,38 @@ +import request from '@/config/axios' + +/** IoT OTA 任务记录信息 */ +export interface OtaTaskRecord { + id?: number // 升级记录编号 + firmwareId?: number // 固件编号 + firmwareVersion?: string // 固件版本 + taskId?: number // 任务编号 + deviceId?: string // 设备编号 + deviceName?: string // 设备名称 + currentVersion?: string // 当前版本 + fromFirmwareId?: number // 来源的固件编号 + fromFirmwareVersion?: string // 来源的固件版本 + status?: number // 升级状态 + progress?: number // 升级进度,百分比 + description?: string // 升级进度描述 + updateTime?: Date // 更新时间 +} + +// IoT OTA 任务记录 API +export const IoTOtaTaskRecordApi = { + getOtaTaskRecordStatusStatistics: async (firmwareId?: number, taskId?: number) => { + const params: any = {} + if (firmwareId) params.firmwareId = firmwareId + if (taskId) params.taskId = taskId + return await request.get({ url: `/iot/ota/task/record/get-status-statistics`, params }) + }, + + // 查询 OTA 任务记录分页 + getOtaTaskRecordPage: async (params: any) => { + return await request.get({ url: `/iot/ota/task/record/page`, params }) + }, + + // 取消 OTA 任务记录 + cancelOtaTaskRecord: async (id: number) => { + return await request.put({ url: `/iot/ota/task/record/cancel?id=` + id }) + } +} diff --git a/src/api/iot/plugin/index.ts b/src/api/iot/plugin/index.ts deleted file mode 100644 index f68b5f94..00000000 --- a/src/api/iot/plugin/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import request from '@/config/axios' - -// IoT 插件配置 VO -export interface PluginConfigVO { - id: number // 主键ID - pluginKey: string // 插件标识 - name: string // 插件名称 - description: string // 描述 - deployType: number // 部署方式 - fileName: string // 插件包文件名 - version: string // 插件版本 - type: number // 插件类型 - protocol: string // 设备插件协议类型 - status: number // 状态 - configSchema: string // 插件配置项描述信息 - config: string // 插件配置信息 - script: string // 插件脚本 -} - -// IoT 插件配置 API -export const PluginConfigApi = { - // 查询插件配置分页 - getPluginConfigPage: async (params: any) => { - return await request.get({ url: `/iot/plugin-config/page`, params }) - }, - - // 查询插件配置详情 - getPluginConfig: async (id: number) => { - return await request.get({ url: `/iot/plugin-config/get?id=` + id }) - }, - - // 新增插件配置 - createPluginConfig: async (data: PluginConfigVO) => { - return await request.post({ url: `/iot/plugin-config/create`, data }) - }, - - // 修改插件配置 - updatePluginConfig: async (data: PluginConfigVO) => { - return await request.put({ url: `/iot/plugin-config/update`, data }) - }, - - // 删除插件配置 - deletePluginConfig: async (id: number) => { - return await request.delete({ url: `/iot/plugin-config/delete?id=` + id }) - }, - - // 修改插件状态 - updatePluginStatus: async (data: any) => { - return await request.put({ url: `/iot/plugin-config/update-status`, data }) - } -} diff --git a/src/api/iot/product/product/index.ts b/src/api/iot/product/product/index.ts index 496fb049..c9f273eb 100644 --- a/src/api/iot/product/product/index.ts +++ b/src/api/iot/product/product/index.ts @@ -11,31 +11,30 @@ export interface ProductVO { icon: string // 产品图标 picUrl: string // 产品图片 description: string // 产品描述 - validateType: number // 数据校验级别 status: number // 产品状态 deviceType: number // 设备类型 + locationType: number // 设备类型 netType: number // 联网方式 - protocolType: number // 接入网关协议 - dataFormat: number // 数据格式 + codecType: string // 数据格式(编解码器类型) deviceCount: number // 设备数量 createTime: Date // 创建时间 } -// IOT 数据校验级别枚举类 -export enum ValidateTypeEnum { - WEAK = 0, // 弱校验 - NONE = 1 // 免校验 -} // IOT 产品设备类型枚举类 0: 直连设备, 1: 网关子设备, 2: 网关设备 export enum DeviceTypeEnum { DEVICE = 0, // 直连设备 GATEWAY_SUB = 1, // 网关子设备 GATEWAY = 2 // 网关设备 } -// IOT 数据格式枚举类 -export enum DataFormatEnum { - JSON = 0, // 标准数据格式(JSON) - CUSTOMIZE = 1 // 透传/自定义 +// IOT 产品定位类型枚举类 0: 手动定位, 1: IP 定位, 2: 定位模块定位 +export enum LocationTypeEnum { + IP = 1, // IP 定位 + MODULE = 2, // 设备定位 + MANUAL = 3 // 手动定位 +} +// IOT 数据格式(编解码器类型)枚举类 +export enum CodecTypeEnum { + ALINK = 'Alink' // 阿里云 Alink 协议 } // IoT 产品 API @@ -78,5 +77,10 @@ export const ProductApi = { // 查询产品(精简)列表 getSimpleProductList() { return request.get({ url: '/iot/product/simple-list' }) + }, + + // 根据 ProductKey 获取产品信息 + getProductByKey: async (productKey: string) => { + return await request.get({ url: `/iot/product/get-by-key`, params: { productKey } }) } } diff --git a/src/api/iot/rule/data/rule/index.ts b/src/api/iot/rule/data/rule/index.ts new file mode 100644 index 00000000..f8059611 --- /dev/null +++ b/src/api/iot/rule/data/rule/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +/** IoT 数据流转规则信息 */ +export interface DataRule { + id: number // 场景编号 + name?: string // 场景名称 + description: string // 场景描述 + status?: number // 场景状态 + sourceConfigs?: any[] // 数据源配置数组 + sinkIds?: number[] // 数据目的编号数组 +} + +// IoT 数据流转规则 API +export const DataRuleApi = { + // 查询数据流转规则分页 + getDataRulePage: async (params: any) => { + return await request.get({ url: `/iot/data-rule/page`, params }) + }, + + // 查询数据流转规则详情 + getDataRule: async (id: number) => { + return await request.get({ url: `/iot/data-rule/get?id=` + id }) + }, + + // 新增数据流转规则 + createDataRule: async (data: DataRule) => { + return await request.post({ url: `/iot/data-rule/create`, data }) + }, + + // 修改数据流转规则 + updateDataRule: async (data: DataRule) => { + return await request.put({ url: `/iot/data-rule/update`, data }) + }, + + // 删除数据流转规则 + deleteDataRule: async (id: number) => { + return await request.delete({ url: `/iot/data-rule/delete?id=` + id }) + } +} diff --git a/src/api/iot/rule/databridge/index.ts b/src/api/iot/rule/data/sink/index.ts similarity index 55% rename from src/api/iot/rule/databridge/index.ts rename to src/api/iot/rule/data/sink/index.ts index d4eb6366..3e2755e0 100644 --- a/src/api/iot/rule/databridge/index.ts +++ b/src/api/iot/rule/data/sink/index.ts @@ -1,7 +1,7 @@ import request from '@/config/axios' -// IoT 数据桥梁 VO -export interface DataBridgeVO { +// IoT 数据流转目的 VO +export interface DataSinkVO { id?: number // 桥梁编号 name?: string // 桥梁名称 description?: string // 桥梁描述 @@ -79,49 +79,48 @@ export interface RedisStreamMQConfig extends Config { topic: string } -/** 数据桥梁类型 */ -// TODO @puhui999:枚举用 number 可以么? -export const IoTDataBridgeConfigType = { - HTTP: '1', - TCP: '2', - WEBSOCKET: '3', - MQTT: '10', - DATABASE: '20', - REDIS_STREAM: '21', - ROCKETMQ: '30', - RABBITMQ: '31', - KAFKA: '32' +/** 数据流转目的类型 */ +export const IotDataSinkTypeEnum = { + HTTP: 1, + TCP: 2, + WEBSOCKET: 3, + MQTT: 10, + DATABASE: 20, + REDIS_STREAM: 21, + ROCKETMQ: 30, + RABBITMQ: 31, + KAFKA: 32 } as const -// 数据桥梁 API -export const DataBridgeApi = { - // 查询数据桥梁分页 - getDataBridgePage: async (params: any) => { - return await request.get({ url: `/iot/data-bridge/page`, params }) +// 数据流转目的 API +export const DataSinkApi = { + // 查询数据流转目的分页 + getDataSinkPage: async (params: any) => { + return await request.get({ url: `/iot/data-sink/page`, params }) }, - // 查询数据桥梁详情 - getDataBridge: async (id: number) => { - return await request.get({ url: `/iot/data-bridge/get?id=` + id }) + // 查询数据流转目的详情 + getDataSink: async (id: number) => { + return await request.get({ url: `/iot/data-sink/get?id=` + id }) }, - // 新增数据桥梁 - createDataBridge: async (data: DataBridgeVO) => { - return await request.post({ url: `/iot/data-bridge/create`, data }) + // 新增数据流转目的 + createDataSink: async (data: DataSinkVO) => { + return await request.post({ url: `/iot/data-sink/create`, data }) }, - // 修改数据桥梁 - updateDataBridge: async (data: DataBridgeVO) => { - return await request.put({ url: `/iot/data-bridge/update`, data }) + // 修改数据流转目的 + updateDataSink: async (data: DataSinkVO) => { + return await request.put({ url: `/iot/data-sink/update`, data }) }, - // 删除数据桥梁 - deleteDataBridge: async (id: number) => { - return await request.delete({ url: `/iot/data-bridge/delete?id=` + id }) + // 删除数据流转目的 + deleteDataSink: async (id: number) => { + return await request.delete({ url: `/iot/data-sink/delete?id=` + id }) }, - // 导出数据桥梁 Excel - exportDataBridge: async (params) => { - return await request.download({ url: `/iot/data-bridge/export-excel`, params }) + // 查询数据流转目的(精简)列表 + getDataSinkSimpleList() { + return request.get({ url: '/iot/data-sink/simple-list' }) } } diff --git a/src/api/iot/rule/scene/index.ts b/src/api/iot/rule/scene/index.ts new file mode 100644 index 00000000..0fbde264 --- /dev/null +++ b/src/api/iot/rule/scene/index.ts @@ -0,0 +1,87 @@ +import request from '@/config/axios' + +// 场景联动 +export interface IotSceneRule { + id?: number // 场景编号 + name: string // 场景名称 + description?: string // 场景描述 + status: number // 场景状态:0-开启,1-关闭 + triggers: Trigger[] // 触发器数组 + actions: Action[] // 执行器数组 +} + +// 触发器结构 +export interface Trigger { + type: number // 触发类型 + productId?: number // 产品编号 + deviceId?: number // 设备编号 + identifier?: string // 物模型标识符 + operator?: string // 操作符 + value?: string // 参数值 + cronExpression?: string // CRON 表达式 + conditionGroups?: TriggerCondition[][] // 条件组(二维数组) +} + +// 触发条件结构 +export interface TriggerCondition { + type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间 + productId?: number // 产品编号 + deviceId?: number // 设备编号 + identifier?: string // 标识符 + operator: string // 操作符 + param: string // 参数 +} + +// 执行器结构 +export interface Action { + type: number // 执行类型 + productId?: number // 产品编号 + deviceId?: number // 设备编号 + identifier?: string // 物模型标识符(服务调用时使用) + params?: string // 请求参数 + alertConfigId?: number // 告警配置编号 +} + +// IoT 场景联动 API +export const RuleSceneApi = { + // 查询场景联动分页 + getRuleScenePage: async (params: any) => { + return await request.get({ url: `/iot/scene-rule/page`, params }) + }, + + // 查询场景联动详情 + getRuleScene: async (id: number) => { + return await request.get({ url: `/iot/scene-rule/get?id=` + id }) + }, + + // 新增场景联动 + createRuleScene: async (data: IotSceneRule) => { + return await request.post({ url: `/iot/scene-rule/create`, data }) + }, + + // 修改场景联动 + updateRuleScene: async (data: IotSceneRule) => { + return await request.put({ url: `/iot/scene-rule/update`, data }) + }, + + // 修改场景联动 + updateRuleSceneStatus: async (id: number, status: number) => { + return await request.put({ + url: `/iot/scene-rule/update-status`, + data: { + id, + status + } + }) + }, + + // 删除场景联动 + deleteRuleScene: async (id: number) => { + return await request.delete({ url: `/iot/scene-rule/delete?id=` + id }) + }, + + // 获取场景联动简单列表 + getSimpleRuleSceneList: async () => { + return await request.get({ url: `/iot/scene-rule/simple-list` }) + } +} diff --git a/src/api/iot/statistics/index.ts b/src/api/iot/statistics/index.ts index 1ca00d65..cdcb94d2 100644 --- a/src/api/iot/statistics/index.ts +++ b/src/api/iot/statistics/index.ts @@ -16,25 +16,44 @@ export interface IotStatisticsSummaryRespVO { productCategoryDeviceCounts: Record } +/** 时间戳-数值的键值对类型 */ +interface TimeValueItem { + [key: string]: number +} + /** IoT 消息统计数据类型 */ export interface IotStatisticsDeviceMessageSummaryRespVO { - upstreamCounts: Record - downstreamCounts: Record + statType: number + upstreamCounts: TimeValueItem[] + downstreamCounts: TimeValueItem[] +} + +/** 新的消息统计数据项 */ +export interface IotStatisticsDeviceMessageSummaryByDateRespVO { + time: string + upstreamCount: number + downstreamCount: number +} + +/** 新的消息统计接口参数 */ +export interface IotStatisticsDeviceMessageReqVO { + interval: number + times?: string[] } // IoT 数据统计 API -export const ProductCategoryApi = { - // 查询基础的数据统计 - getIotStatisticsSummary: async () => { +export const StatisticsApi = { + // 查询全局的数据统计 + getStatisticsSummary: async () => { return await request.get({ url: `/iot/statistics/get-summary` }) }, - // 查询设备上下行消息的数据统计 - getIotStatisticsDeviceMessageSummary: async (params: { startTime: number; endTime: number }) => { - return await request.get({ - url: `/iot/statistics/get-log-summary`, + // 获取设备消息的数据统计 + getDeviceMessageSummaryByDate: async (params: IotStatisticsDeviceMessageReqVO) => { + return await request.get({ + url: `/iot/statistics/get-device-message-summary-by-date`, params }) } diff --git a/src/api/iot/thingmodel/index.ts b/src/api/iot/thingmodel/index.ts index 5deaaad5..bcf9e070 100644 --- a/src/api/iot/thingmodel/index.ts +++ b/src/api/iot/thingmodel/index.ts @@ -1,4 +1,5 @@ import request from '@/config/axios' +import { isEmpty } from '@/utils/is' /** * IoT 产品物模型 @@ -17,14 +18,6 @@ export interface ThingModelData { service?: ThingModelService // 服务 } -/** - * IoT 模拟设备 - */ -// TODO @super:和 ThingModelSimulatorData 会不会好点 -export interface SimulatorData extends ThingModelData { - simulateValue?: string | number // 用于存储模拟值 TODO @super:字段使用 value 会不会好点 -} - /** * ThingModelProperty 类型 */ @@ -46,6 +39,127 @@ export interface ThingModelService { [key: string]: any } +/** dataSpecs 数值型数据结构 */ +export interface DataSpecsNumberData { + dataType: 'int' | 'float' | 'double' // 数据类型,取值为 INT、FLOAT 或 DOUBLE + max: string // 最大值,必须与 dataType 设置一致,且为 STRING 类型 + min: string // 最小值,必须与 dataType 设置一致,且为 STRING 类型 + step: string // 步长,必须与 dataType 设置一致,且为 STRING 类型 + precise?: string // 精度,当 dataType 为 FLOAT 或 DOUBLE 时可选 + defaultValue?: string // 默认值,可选 + unit: string // 单位的符号 + unitName: string // 单位的名称 +} + +/** dataSpecs 枚举型数据结构 */ +export interface DataSpecsEnumOrBoolData { + dataType: 'enum' | 'bool' + defaultValue?: string // 默认值,可选 + name: string // 枚举项的名称 + value: number | undefined // 枚举值 +} + +/** 物模型TSL响应数据结构 */ +export interface IotThingModelTSLResp { + productId: number + productKey: string + properties: ThingModelProperty[] + events: ThingModelEvent[] + services: ThingModelService[] +} + +/** 物模型属性 */ +export interface ThingModelProperty { + identifier: string + name: string + accessMode: string + required?: boolean + dataType: string + description?: string + dataSpecs?: ThingModelProperty + dataSpecsList?: ThingModelProperty[] +} + +/** 物模型事件 */ +export interface ThingModelEvent { + identifier: string + name: string + required?: boolean + type: string + description?: string + outputParams?: ThingModelParam[] + method?: string +} + +/** 物模型服务 */ +export interface ThingModelService { + identifier: string + name: string + required?: boolean + callType: string + description?: string + inputParams?: ThingModelParam[] + outputParams?: ThingModelParam[] + method?: string +} + +/** 物模型参数 */ +export interface ThingModelParam { + identifier: string + name: string + direction: string + paraOrder?: number + dataType: string + dataSpecs?: ThingModelProperty + dataSpecsList?: ThingModelProperty[] +} + +/** 数值型数据规范 */ +export interface ThingModelNumericDataSpec { + dataType: 'int' | 'float' | 'double' + max: string + min: string + step: string + precise?: string + defaultValue?: string + unit?: string + unitName?: string +} + +/** 布尔/枚举型数据规范 */ +export interface ThingModelBoolOrEnumDataSpecs { + dataType: 'bool' | 'enum' + name: string + value: number +} + +/** 文本/时间型数据规范 */ +export interface ThingModelDateOrTextDataSpecs { + dataType: 'text' | 'date' + length?: number + defaultValue?: string +} + +/** 数组型数据规范 */ +export interface ThingModelArrayDataSpecs { + dataType: 'array' + size: number + childDataType: string + dataSpecsList?: ThingModelProperty[] +} + +/** 结构体型数据规范 */ +export interface ThingModelStructDataSpecs { + dataType: 'struct' + identifier: string + name: string + accessMode: string + required?: boolean + childDataType: string + dataSpecs?: ThingModelProperty + dataSpecsList?: ThingModelProperty[] +} + // IoT 产品物模型 API export const ThingModelApi = { // 查询产品物模型分页 @@ -58,11 +172,10 @@ export const ThingModelApi = { return await request.get({ url: `/iot/thing-model/list`, params }) }, - // 获得产品物模型 - getThingModelListByProductId: async (params: any) => { + // 获得产品物模型 TSL + getThingModelTSLByProductId: async (productId: number) => { return await request.get({ - url: `/iot/thing-model/list-by-product-id`, - params + url: `/iot/thing-model/get-tsl?productId=${productId}` }) }, @@ -86,3 +199,103 @@ export const ThingModelApi = { return await request.delete({ url: `/iot/thing-model/delete?id=` + id }) } } + +/** 公共校验规则 */ +export const ThingModelFormRules = { + name: [ + { required: true, message: '功能名称不能为空', trigger: 'blur' }, + { + pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/, + message: + '支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符', + trigger: 'blur' + } + ], + type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }], + identifier: [ + { required: true, message: '标识符不能为空', trigger: 'blur' }, + { + pattern: /^[a-zA-Z0-9_]{1,50}$/, + message: '支持大小写字母、数字和下划线,不超过 50 个字符', + trigger: 'blur' + }, + { + validator: (_: any, value: string, callback: any) => { + const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value'] + if (reservedKeywords.includes(value)) { + callback( + new Error( + 'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义' + ) + ) + } else if (/^\d+$/.test(value)) { + callback(new Error('标识符不能是纯数字')) + } else { + callback() + } + }, + trigger: 'blur' + } + ], + 'property.dataSpecs.childDataType': [{ required: true, message: '元素类型不能为空' }], + 'property.dataSpecs.size': [ + { required: true, message: '元素个数不能为空' }, + { + validator: (_: any, value: any, callback: any) => { + if (isEmpty(value)) { + callback(new Error('元素个数不能为空')) + return + } + if (isNaN(Number(value))) { + callback(new Error('元素个数必须是数字')) + return + } + callback() + }, + trigger: 'blur' + } + ], + 'property.dataSpecs.length': [ + { required: true, message: '请输入文本字节长度', trigger: 'blur' }, + { + validator: (_: any, value: any, callback: any) => { + if (isEmpty(value)) { + callback(new Error('文本长度不能为空')) + return + } + if (isNaN(Number(value))) { + callback(new Error('文本长度必须是数字')) + return + } + callback() + }, + trigger: 'blur' + } + ], + 'property.accessMode': [{ required: true, message: '请选择读写类型', trigger: 'change' }] +} + +/** 校验布尔值名称 */ +export const validateBoolName = (_: any, value: string, callback: any) => { + if (isEmpty(value)) { + callback(new Error('布尔值名称不能为空')) + return + } + // 检查开头字符 + if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) { + callback(new Error('布尔值名称必须以中文、英文字母或数字开头')) + return + } + // 检查整体格式 + if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) { + callback(new Error('布尔值名称只能包含中文、英文字母、数字、下划线和短划线')) + return + } + // 检查长度(一个中文算一个字符) + if (value.length > 20) { + callback(new Error('布尔值名称长度不能超过 20 个字符')) + return + } + + callback() +} diff --git a/src/api/system/role/index.ts b/src/api/system/role/index.ts index aaba4eab..b6d3bbb0 100644 --- a/src/api/system/role/index.ts +++ b/src/api/system/role/index.ts @@ -12,11 +12,6 @@ export interface RoleVO { createTime: Date } -export interface UpdateStatusReqVO { - id: number - status: number -} - // 查询角色列表 export const getRolePage = async (params: PageParam) => { return await request.get({ url: '/system/role/page', params }) @@ -42,11 +37,6 @@ export const updateRole = async (data: RoleVO) => { return await request.put({ url: '/system/role/update', data }) } -// 修改角色状态 -export const updateRoleStatus = async (data: UpdateStatusReqVO) => { - return await request.put({ url: '/system/role/update-status', data }) -} - // 删除角色 export const deleteRole = async (id: number) => { return await request.delete({ url: '/system/role/delete?id=' + id }) @@ -58,7 +48,7 @@ export const deleteRoleList = async (ids: number[]) => { } // 导出角色 -export const exportRole = (params) => { +export const exportRole = (params: any) => { return request.download({ url: '/system/role/export-excel', params diff --git a/src/components/JsonEditor/index.ts b/src/components/JsonEditor/index.ts new file mode 100644 index 00000000..037b4496 --- /dev/null +++ b/src/components/JsonEditor/index.ts @@ -0,0 +1,3 @@ +import JsonEditor from './src/JsonEditor.vue' + +export { JsonEditor } diff --git a/src/components/JsonEditor/src/JsonEditor.vue b/src/components/JsonEditor/src/JsonEditor.vue new file mode 100644 index 00000000..f3789ee1 --- /dev/null +++ b/src/components/JsonEditor/src/JsonEditor.vue @@ -0,0 +1,126 @@ + + + + diff --git a/src/components/JsonEditor/types/index.ts b/src/components/JsonEditor/types/index.ts new file mode 100644 index 00000000..b7dadd77 --- /dev/null +++ b/src/components/JsonEditor/types/index.ts @@ -0,0 +1,80 @@ +import { JSONEditorOptions, JSONEditorMode } from 'jsoneditor' + +export interface JsonEditorProps { + /** + * JSON数据,支持双向绑定 + */ + modelValue: any + + /** + * 编辑器模式 + * @default 'tree' + */ + mode?: JSONEditorMode + + /** + * 编辑器高度 + * @default '400px' + */ + height?: string + + /** + * 是否显示模式选择下拉菜单 + * @default false + */ + showModeSelection?: boolean + + /** + * 是否显示导航栏 + * @default false + */ + showNavigationBar?: boolean + + /** + * 是否显示状态栏 + * @default true + */ + showStatusBar?: boolean + + /** + * 是否显示主菜单栏 + * @default true + */ + showMainMenuBar?: boolean + + /** + * JSONEditor配置选项 + * @see https://github.com/josdejong/jsoneditor/blob/develop/docs/api.md + */ + options?: Partial +} + +/** + * JsonEditor组件触发的事件 + */ +export interface JsonEditorEmits { + /** + * 数据更新时触发 + */ + (e: 'update:modelValue', value: any): void + + /** + * 数据变化时触发 + */ + (e: 'change', value: any): void + + /** + * 验证错误时触发 + */ + (e: 'error', errors: any): void +} + +/** + * JsonEditor组件暴露的方法 + */ +export interface JsonEditorExpose { + /** + * 获取原始的JSONEditor实例 + */ + getEditor: () => any +} diff --git a/src/components/Map/index.vue b/src/components/Map/index.vue new file mode 100644 index 00000000..7b9ae1cf --- /dev/null +++ b/src/components/Map/index.vue @@ -0,0 +1,268 @@ + + + + + + + diff --git a/src/plugins/echarts/index.ts b/src/plugins/echarts/index.ts index 18d05aab..3a402dea 100644 --- a/src/plugins/echarts/index.ts +++ b/src/plugins/echarts/index.ts @@ -13,6 +13,7 @@ import { import { AriaComponent, + DataZoomComponent, GridComponent, LegendComponent, ParallelComponent, @@ -30,6 +31,7 @@ echarts.use([ TitleComponent, TooltipComponent, ToolboxComponent, + DataZoomComponent, GridComponent, PolarComponent, AriaComponent, diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index b8828b53..794778ca 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -735,15 +735,15 @@ const remainingRouter: AppRouteRecordRaw[] = [ component: () => import('@/views/iot/device/device/detail/index.vue') }, { - path: 'plugin/detail/:id', - name: 'IoTPluginDetail', + path: 'ota/operation/firmware/detail/:id', + name: 'IoTOtaFirmwareDetail', meta: { - title: '插件详情', + title: '固件详情', noCache: true, hidden: true, - activeMenu: '/iot/plugin' + activeMenu: '/iot/operation/ota/firmware' }, - component: () => import('@/views/iot/plugin/detail/index.vue') + component: () => import('@/views/iot/ota/firmware/detail/index.vue') } ] } diff --git a/src/utils/cron.ts b/src/utils/cron.ts new file mode 100644 index 00000000..ee132f00 --- /dev/null +++ b/src/utils/cron.ts @@ -0,0 +1,471 @@ +/** + * CRON 表达式工具类 + * 提供 CRON 表达式的解析、格式化、验证等功能 + */ + +/** CRON 字段类型枚举 */ +export enum CronFieldType { + SECOND = 'second', + MINUTE = 'minute', + HOUR = 'hour', + DAY = 'day', + MONTH = 'month', + WEEK = 'week', + YEAR = 'year' +} + +/** CRON 字段配置 */ +export interface CronFieldConfig { + key: CronFieldType + label: string + min: number + max: number + names?: Record // 名称映射,如月份名称 +} + +/** CRON 字段配置常量 */ +export const CRON_FIELD_CONFIGS: Record = { + [CronFieldType.SECOND]: { key: CronFieldType.SECOND, label: '秒', min: 0, max: 59 }, + [CronFieldType.MINUTE]: { key: CronFieldType.MINUTE, label: '分', min: 0, max: 59 }, + [CronFieldType.HOUR]: { key: CronFieldType.HOUR, label: '时', min: 0, max: 23 }, + [CronFieldType.DAY]: { key: CronFieldType.DAY, label: '日', min: 1, max: 31 }, + [CronFieldType.MONTH]: { + key: CronFieldType.MONTH, + label: '月', + min: 1, + max: 12, + names: { + JAN: 1, + FEB: 2, + MAR: 3, + APR: 4, + MAY: 5, + JUN: 6, + JUL: 7, + AUG: 8, + SEP: 9, + OCT: 10, + NOV: 11, + DEC: 12 + } + }, + [CronFieldType.WEEK]: { + key: CronFieldType.WEEK, + label: '周', + min: 0, + max: 7, + names: { + SUN: 0, + MON: 1, + TUE: 2, + WED: 3, + THU: 4, + FRI: 5, + SAT: 6 + } + }, + [CronFieldType.YEAR]: { key: CronFieldType.YEAR, label: '年', min: 1970, max: 2099 } +} + +/** 解析后的 CRON 字段 */ +export interface ParsedCronField { + type: 'any' | 'specific' | 'range' | 'step' | 'list' | 'last' | 'weekday' | 'nth' + values: number[] + original: string + description: string +} + +/** 解析后的 CRON 表达式 */ +export interface ParsedCronExpression { + second: ParsedCronField + minute: ParsedCronField + hour: ParsedCronField + day: ParsedCronField + month: ParsedCronField + week: ParsedCronField + year?: ParsedCronField + isValid: boolean + description: string + nextExecutionTime?: Date +} + +/** 常用 CRON 表达式预设 */ +export const CRON_PRESETS = { + EVERY_SECOND: '* * * * * ?', + EVERY_MINUTE: '0 * * * * ?', + EVERY_HOUR: '0 0 * * * ?', + EVERY_DAY: '0 0 0 * * ?', + EVERY_WEEK: '0 0 0 ? * 1', + EVERY_MONTH: '0 0 0 1 * ?', + EVERY_YEAR: '0 0 0 1 1 ?', + WORKDAY_9AM: '0 0 9 ? * 2-6', // 工作日上午9点 + WORKDAY_6PM: '0 0 18 ? * 2-6', // 工作日下午6点 + WEEKEND_10AM: '0 0 10 ? * 1,7' // 周末上午10点 +} as const + +/** CRON 表达式工具类 */ +export class CronUtils { + /** 验证 CRON 表达式格式 */ + static validate(cronExpression: string): boolean { + if (!cronExpression || typeof cronExpression !== 'string') { + return false + } + + const parts = cronExpression.trim().split(/\s+/) + + // 支持 5-7 个字段的 CRON 表达式 + if (parts.length < 5 || parts.length > 7) { + return false + } + + // 基本格式验证 + const cronRegex = /^[0-9*\/\-,?LW#]+$/ + return parts.every((part) => cronRegex.test(part)) + } + + /** 解析单个 CRON 字段 */ + static parseField( + fieldValue: string, + fieldType: CronFieldType, + config: CronFieldConfig + ): ParsedCronField { + const field: ParsedCronField = { + type: 'any', + values: [], + original: fieldValue, + description: '' + } + + // 处理特殊字符 + if (fieldValue === '*' || fieldValue === '?') { + field.type = 'any' + field.description = `每${config.label}` + return field + } + + // 处理最后一天 (L) + if (fieldValue === 'L' && fieldType === CronFieldType.DAY) { + field.type = 'last' + field.description = '每月最后一天' + return field + } + + // 处理范围 (-) + if (fieldValue.includes('-')) { + const [start, end] = fieldValue.split('-').map(Number) + if (!isNaN(start) && !isNaN(end) && start >= config.min && end <= config.max) { + field.type = 'range' + field.values = Array.from({ length: end - start + 1 }, (_, i) => start + i) + field.description = `${config.label} ${start}-${end}` + } + return field + } + + // 处理步长 (/) + if (fieldValue.includes('/')) { + const [base, step] = fieldValue.split('/') + const stepNum = Number(step) + if (!isNaN(stepNum) && stepNum > 0) { + field.type = 'step' + if (base === '*') { + field.description = `每${stepNum}${config.label}` + } else { + const startNum = Number(base) + field.description = `从${startNum}开始每${stepNum}${config.label}` + } + } + return field + } + + // 处理列表 (,) + if (fieldValue.includes(',')) { + const values = fieldValue + .split(',') + .map(Number) + .filter((n) => !isNaN(n)) + if (values.length > 0) { + field.type = 'list' + field.values = values + field.description = `${config.label} ${values.join(',')}` + } + return field + } + + // 处理具体数值 + const numValue = Number(fieldValue) + if (!isNaN(numValue) && numValue >= config.min && numValue <= config.max) { + field.type = 'specific' + field.values = [numValue] + field.description = `${config.label} ${numValue}` + } + + return field + } + + /** 解析完整的 CRON 表达式 */ + static parse(cronExpression: string): ParsedCronExpression { + const result: ParsedCronExpression = { + second: { type: 'any', values: [], original: '*', description: '每秒' }, + minute: { type: 'any', values: [], original: '*', description: '每分' }, + hour: { type: 'any', values: [], original: '*', description: '每时' }, + day: { type: 'any', values: [], original: '*', description: '每日' }, + month: { type: 'any', values: [], original: '*', description: '每月' }, + week: { type: 'any', values: [], original: '?', description: '任意周' }, + isValid: false, + description: '' + } + + if (!this.validate(cronExpression)) { + result.description = '无效的 CRON 表达式' + return result + } + + const parts = cronExpression.trim().split(/\s+/) + const fieldTypes = [ + CronFieldType.SECOND, + CronFieldType.MINUTE, + CronFieldType.HOUR, + CronFieldType.DAY, + CronFieldType.MONTH, + CronFieldType.WEEK + ] + + // 如果只有5个字段,则第一个字段是分钟 + const startIndex = parts.length === 5 ? 1 : 0 + + for (let i = 0; i < parts.length; i++) { + const fieldType = fieldTypes[i + startIndex] + if (fieldType && CRON_FIELD_CONFIGS[fieldType]) { + const config = CRON_FIELD_CONFIGS[fieldType] + result[fieldType] = this.parseField(parts[i], fieldType, config) + } + } + + // 处理年份字段(如果存在) + if (parts.length === 7) { + const yearConfig = CRON_FIELD_CONFIGS[CronFieldType.YEAR] + result.year = this.parseField(parts[6], CronFieldType.YEAR, yearConfig) + } + + result.isValid = true + result.description = this.generateDescription(result) + + return result + } + + /** 生成 CRON 表达式的可读描述 */ + static generateDescription(parsed: ParsedCronExpression): string { + const parts: string[] = [] + + // 构建时间部分描述 + if (parsed.hour.type === 'specific' && parsed.minute.type === 'specific') { + const hour = parsed.hour.values[0] + const minute = parsed.minute.values[0] + parts.push(`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`) + } else if (parsed.hour.type === 'specific') { + parts.push(`每天${parsed.hour.values[0]}点`) + } else if (parsed.minute.type === 'specific' && parsed.minute.values[0] === 0) { + if (parsed.hour.type === 'any') { + parts.push('每小时整点') + } + } else if (parsed.minute.type === 'step') { + const step = parsed.minute.original.split('/')[1] + parts.push(`每${step}分钟`) + } else if (parsed.hour.type === 'step') { + const step = parsed.hour.original.split('/')[1] + parts.push(`每${step}小时`) + } + + // 构建日期部分描述 + if (parsed.day.type === 'specific') { + parts.push(`每月${parsed.day.values[0]}日`) + } else if (parsed.week.type === 'specific') { + const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + const weekDay = parsed.week.values[0] + if (weekDay >= 0 && weekDay <= 6) { + parts.push(`每${weekNames[weekDay]}`) + } + } else if (parsed.week.type === 'range') { + parts.push('工作日') + } + + // 构建月份部分描述 + if (parsed.month.type === 'specific') { + parts.push(`${parsed.month.values[0]}月`) + } + + return parts.length > 0 ? parts.join(' ') : '自定义时间规则' + } + + /** 格式化 CRON 表达式为可读文本 */ + static format(cronExpression: string): string { + if (!cronExpression) return '' + + const parsed = this.parse(cronExpression) + return parsed.isValid ? parsed.description : cronExpression + } + + /** 获取预设的 CRON 表达式列表 */ + static getPresets() { + return Object.entries(CRON_PRESETS).map(([key, value]) => ({ + label: this.format(value), + value, + key + })) + } + + /** 计算 CRON 表达式的下次执行时间 */ + static getNextExecutionTime(cronExpression: string, fromDate?: Date): Date | null { + const parsed = this.parse(cronExpression) + if (!parsed.isValid) { + return null + } + + const now = fromDate || new Date() + // eslint-disable-next-line prefer-const + let nextTime = new Date(now.getTime() + 1000) // 从下一秒开始 + + // 简化版本:处理常见的 CRON 表达式模式 + // 对于复杂的 CRON 表达式,建议使用专门的库如 node-cron 或 cron-parser + + // 处理每分钟执行 + if (parsed.second.type === 'specific' && parsed.minute.type === 'any') { + const targetSecond = parsed.second.values[0] + nextTime.setSeconds(targetSecond, 0) + if (nextTime <= now) { + nextTime.setMinutes(nextTime.getMinutes() + 1) + } + return nextTime + } + + // 处理每小时执行 + if ( + parsed.second.type === 'specific' && + parsed.minute.type === 'specific' && + parsed.hour.type === 'any' + ) { + const targetSecond = parsed.second.values[0] + const targetMinute = parsed.minute.values[0] + nextTime.setMinutes(targetMinute, targetSecond, 0) + if (nextTime <= now) { + nextTime.setHours(nextTime.getHours() + 1) + } + return nextTime + } + + // 处理每天执行 + if ( + parsed.second.type === 'specific' && + parsed.minute.type === 'specific' && + parsed.hour.type === 'specific' + ) { + const targetSecond = parsed.second.values[0] + const targetMinute = parsed.minute.values[0] + const targetHour = parsed.hour.values[0] + + nextTime.setHours(targetHour, targetMinute, targetSecond, 0) + if (nextTime <= now) { + nextTime.setDate(nextTime.getDate() + 1) + } + return nextTime + } + + // 处理步长执行 + if (parsed.minute.type === 'step') { + const step = parseInt(parsed.minute.original.split('/')[1]) + const currentMinute = nextTime.getMinutes() + const nextMinute = Math.ceil(currentMinute / step) * step + + if (nextMinute >= 60) { + nextTime.setHours(nextTime.getHours() + 1, 0, 0, 0) + } else { + nextTime.setMinutes(nextMinute, 0, 0) + } + return nextTime + } + + // 对于其他复杂情况,返回一个估算时间 + return new Date(now.getTime() + 60000) // 1分钟后 + } + + /** 获取 CRON 表达式的执行频率描述 */ + static getFrequencyDescription(cronExpression: string): string { + const parsed = this.parse(cronExpression) + if (!parsed.isValid) { + return '无效表达式' + } + + // 计算大概的执行频率 + if (parsed.second.type === 'any' && parsed.minute.type === 'any') { + return '每秒执行' + } + + if (parsed.minute.type === 'any' && parsed.hour.type === 'any') { + return '每分钟执行' + } + + if (parsed.hour.type === 'any' && parsed.day.type === 'any') { + return '每小时执行' + } + + if (parsed.day.type === 'any' && parsed.month.type === 'any') { + return '每天执行' + } + + if (parsed.month.type === 'any') { + return '每月执行' + } + + return '按计划执行' + } + + /** 检查 CRON 表达式是否会在指定时间执行 */ + static willExecuteAt(cronExpression: string, targetDate: Date): boolean { + const parsed = this.parse(cronExpression) + if (!parsed.isValid) { + return false + } + + // 检查各个字段是否匹配 + const second = targetDate.getSeconds() + const minute = targetDate.getMinutes() + const hour = targetDate.getHours() + const day = targetDate.getDate() + const month = targetDate.getMonth() + 1 + const weekDay = targetDate.getDay() + + return ( + this.fieldMatches(parsed.second, second) && + this.fieldMatches(parsed.minute, minute) && + this.fieldMatches(parsed.hour, hour) && + this.fieldMatches(parsed.day, day) && + this.fieldMatches(parsed.month, month) && + (parsed.week.type === 'any' || this.fieldMatches(parsed.week, weekDay)) + ) + } + + /** 检查字段值是否匹配 */ + private static fieldMatches(field: ParsedCronField, value: number): boolean { + if (field.type === 'any') { + return true + } + + if (field.type === 'specific' || field.type === 'list') { + return field.values.includes(value) + } + + if (field.type === 'range') { + return value >= field.values[0] && value <= field.values[field.values.length - 1] + } + + if (field.type === 'step') { + const [base, step] = field.original.split('/').map(Number) + if (base === 0 || field.original.startsWith('*')) { + return value % step === 0 + } + return value >= base && (value - base) % step === 0 + } + + return false + } +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 41128562..b4f0c587 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -231,19 +231,21 @@ export enum DICT_TYPE { // ========== IOT - 物联网模块 ========== IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式 - IOT_VALIDATE_TYPE = 'iot_validate_type', // IOT 数据校验级别 IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态 IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型 - IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式 - IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议 + IOT_CODEC_TYPE = 'iot_codec_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 产品功能类型 - IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型 IOT_THING_MODEL_UNIT = 'iot_thing_model_unit', // IOT 物模型单位 IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型 - IOT_PLUGIN_DEPLOY_TYPE = 'iot_plugin_deploy_type', // IOT 插件部署类型 - IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态 - IOT_PLUGIN_TYPE = 'iot_plugin_type', // IOT 插件类型 - IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向 - IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum' // 桥梁类型 + // TODO @芋艿:貌似这几个多了 _enum 后缀 + IOT_DATA_SINK_TYPE_ENUM = 'iot_data_sink_type_enum', // IoT 数据流转目的类型 + IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举 + IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum', // IoT 规则场景的触发类型枚举 + IOT_ALERT_LEVEL = 'iot_alert_level', // IoT 告警级别 + 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 记录状态 } diff --git a/src/views/iot/alert/config/AlertConfigForm.vue b/src/views/iot/alert/config/AlertConfigForm.vue new file mode 100644 index 00000000..bece0fff --- /dev/null +++ b/src/views/iot/alert/config/AlertConfigForm.vue @@ -0,0 +1,201 @@ + + diff --git a/src/views/iot/alert/config/index.vue b/src/views/iot/alert/config/index.vue new file mode 100644 index 00000000..03504ddb --- /dev/null +++ b/src/views/iot/alert/config/index.vue @@ -0,0 +1,210 @@ + + + diff --git a/src/views/iot/alert/record/index.vue b/src/views/iot/alert/record/index.vue new file mode 100644 index 00000000..e0bef5c9 --- /dev/null +++ b/src/views/iot/alert/record/index.vue @@ -0,0 +1,296 @@ + + + diff --git a/src/views/iot/device/device/DeviceForm.vue b/src/views/iot/device/device/DeviceForm.vue index cf6e92ac..dfed0c63 100644 --- a/src/views/iot/device/device/DeviceForm.vue +++ b/src/views/iot/device/device/DeviceForm.vue @@ -23,19 +23,6 @@ /> - - - - - + + + + {{ dict.label }} + + + + + @@ -91,9 +116,11 @@ diff --git a/src/views/iot/device/device/components/DeviceTableSelect.vue b/src/views/iot/device/device/components/DeviceTableSelect.vue new file mode 100644 index 00000000..73c252da --- /dev/null +++ b/src/views/iot/device/device/components/DeviceTableSelect.vue @@ -0,0 +1,303 @@ + + + + diff --git a/src/views/iot/device/device/detail/DeviceDataDetail.vue b/src/views/iot/device/device/detail/DeviceDataDetail.vue deleted file mode 100644 index ced2a8a4..00000000 --- a/src/views/iot/device/device/detail/DeviceDataDetail.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - diff --git a/src/views/iot/device/device/detail/DeviceDetailConfig.vue b/src/views/iot/device/device/detail/DeviceDetailConfig.vue index 13906b57..d382f79c 100644 --- a/src/views/iot/device/device/detail/DeviceDetailConfig.vue +++ b/src/views/iot/device/device/detail/DeviceDetailConfig.vue @@ -8,24 +8,10 @@ class="my-4" description="如需编辑文件,请点击下方编辑按钮" /> - - - - -
@@ -34,15 +20,20 @@ 保存 编辑 - + + 配置推送 +
diff --git a/src/views/iot/device/device/detail/DeviceDetailsInfo.vue b/src/views/iot/device/device/detail/DeviceDetailsInfo.vue index 7b64a8a6..39a226d4 100644 --- a/src/views/iot/device/device/detail/DeviceDetailsInfo.vue +++ b/src/views/iot/device/device/detail/DeviceDetailsInfo.vue @@ -1,111 +1,168 @@ diff --git a/src/views/iot/device/device/detail/DeviceDetailsLog.vue b/src/views/iot/device/device/detail/DeviceDetailsLog.vue deleted file mode 100644 index 8bbad400..00000000 --- a/src/views/iot/device/device/detail/DeviceDetailsLog.vue +++ /dev/null @@ -1,166 +0,0 @@ - - - - diff --git a/src/views/iot/device/device/detail/DeviceDetailsMessage.vue b/src/views/iot/device/device/detail/DeviceDetailsMessage.vue new file mode 100644 index 00000000..aebb03bf --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsMessage.vue @@ -0,0 +1,201 @@ + + + + diff --git a/src/views/iot/device/device/detail/DeviceDetailsModel.vue b/src/views/iot/device/device/detail/DeviceDetailsModel.vue deleted file mode 100644 index 26ddaa54..00000000 --- a/src/views/iot/device/device/detail/DeviceDetailsModel.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue b/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue index 0ce918aa..599de70d 100644 --- a/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue +++ b/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue @@ -6,75 +6,109 @@ - - + + - + - - - - - - - + + + + - + - - + - -
- 发送 +
+ + 设置属性值后,点击「发送属性上报」按钮 + + 发送属性上报
- - + - - - +
@@ -90,39 +124,106 @@ - - - + + - + - +
+ + 设置属性值后,点击「发送属性设置」按钮 + + 发送属性设置 +
- - + - + + + + + + + + + + + + +
@@ -132,11 +233,9 @@ - - - - - + + + @@ -144,188 +243,178 @@ diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModel.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModel.vue new file mode 100644 index 00000000..99698746 --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModel.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue new file mode 100644 index 00000000..04f9a9f5 --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue @@ -0,0 +1,192 @@ + + + + diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue new file mode 100644 index 00000000..e2824365 --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue @@ -0,0 +1,245 @@ + + + diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue new file mode 100644 index 00000000..c913f1de --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue @@ -0,0 +1,216 @@ + + + diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue new file mode 100644 index 00000000..fd845616 --- /dev/null +++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue @@ -0,0 +1,208 @@ + + + + diff --git a/src/views/iot/device/device/detail/index.vue b/src/views/iot/device/device/detail/index.vue index a2bd3dec..3ec756b5 100644 --- a/src/views/iot/device/device/detail/index.vue +++ b/src/views/iot/device/device/detail/index.vue @@ -3,27 +3,30 @@ :loading="loading" :product="product" :device="device" - @refresh="getDeviceData(id)" + @refresh="getDeviceData" /> - - + - - - + + @@ -40,10 +43,11 @@ import { useTagsViewStore } from '@/store/modules/tagsView' import { DeviceApi, DeviceVO } from '@/api/iot/device/device' import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product' +import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel' import DeviceDetailsHeader from './DeviceDetailsHeader.vue' import DeviceDetailsInfo from './DeviceDetailsInfo.vue' -import DeviceDetailsModel from './DeviceDetailsModel.vue' -import DeviceDetailsLog from './DeviceDetailsLog.vue' +import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue' +import DeviceDetailsMessage from './DeviceDetailsMessage.vue' import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue' import DeviceDetailConfig from './DeviceDetailConfig.vue' @@ -51,11 +55,12 @@ defineOptions({ name: 'IoTDeviceDetail' }) const route = useRoute() const message = useMessage() -const id = route.params.id // 将字符串转换为数字 +const id = Number(route.params.id) // 将字符串转换为数字 const loading = ref(true) // 加载中 const product = ref({} as ProductVO) // 产品详情 const device = ref({} as DeviceVO) // 设备详情 const activeTab = ref('info') // 默认激活的标签页 +const thingModelList = ref([]) // 物模型列表数据 /** 获取设备详情 */ const getDeviceData = async () => { @@ -63,6 +68,7 @@ const getDeviceData = async () => { try { device.value = await DeviceApi.getDevice(id) await getProductData(device.value.productId) + await getThingModelList(device.value.productId) } finally { loading.value = false } @@ -73,9 +79,23 @@ const getProductData = async (id: number) => { product.value = await ProductApi.getProduct(id) } +/** 获取物模型列表 */ +const getThingModelList = async (productId: number) => { + try { + const data = await ThingModelApi.getThingModelList({ + productId: productId + }) + thingModelList.value = data || [] + } catch (error) { + console.error('获取物模型列表失败:', error) + thingModelList.value = [] + } +} + /** 初始化 */ const { delView } = useTagsViewStore() // 视图操作 -const { currentRoute } = useRouter() // 路由 +const router = useRouter() // 路由 +const { currentRoute } = router onMounted(async () => { if (!id) { message.warning('参数错误,产品不能为空!') diff --git a/src/views/iot/device/device/index.vue b/src/views/iot/device/device/index.vue index af382747..56139be9 100644 --- a/src/views/iot/device/device/index.vue +++ b/src/views/iot/device/device/index.vue @@ -199,20 +199,20 @@
所属产品 - + {{ products.find((p) => p.id === item.productId)?.name }} - +
设备类型
- DeviceKey + 备注名称 - {{ item.deviceKey }} + {{ item.nickname || item.deviceName }}
@@ -289,7 +289,9 @@ @@ -442,6 +444,11 @@ const openDetail = (id: number) => { push({ name: 'IoTDeviceDetail', params: { id } }) } +/** 跳转到产品详情页面 */ +const openProductDetail = (productId: number) => { + push({ name: 'IoTProductDetail', params: { id: productId } }) +} + /** 删除按钮操作 */ const handleDelete = async (id: number) => { try { diff --git a/src/views/iot/home/components/ComparisonCard.vue b/src/views/iot/home/components/ComparisonCard.vue new file mode 100644 index 00000000..2da729e1 --- /dev/null +++ b/src/views/iot/home/components/ComparisonCard.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/views/iot/home/components/DeviceCountCard.vue b/src/views/iot/home/components/DeviceCountCard.vue new file mode 100644 index 00000000..5514eff3 --- /dev/null +++ b/src/views/iot/home/components/DeviceCountCard.vue @@ -0,0 +1,131 @@ + + + diff --git a/src/views/iot/home/components/DeviceStateCountCard.vue b/src/views/iot/home/components/DeviceStateCountCard.vue new file mode 100644 index 00000000..fbda0a92 --- /dev/null +++ b/src/views/iot/home/components/DeviceStateCountCard.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/views/iot/home/components/MessageTrendCard.vue b/src/views/iot/home/components/MessageTrendCard.vue new file mode 100644 index 00000000..1d2d4c57 --- /dev/null +++ b/src/views/iot/home/components/MessageTrendCard.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/views/iot/home/index.vue b/src/views/iot/home/index.vue index bee4116b..3d60acef 100644 --- a/src/views/iot/home/index.vue +++ b/src/views/iot/home/index.vue @@ -2,145 +2,61 @@ - -
-
- 分类数量 - -
- - {{ statsData.productCategoryCount }} - - -
- 今日新增 - +{{ statsData.productCategoryTodayCount }} -
-
-
+
- -
-
- 产品数量 - -
- {{ statsData.productCount }} - -
- 今日新增 - +{{ statsData.productTodayCount }} -
-
-
+
- -
-
- 设备数量 - -
- {{ statsData.deviceCount }} - -
- 今日新增 - +{{ statsData.deviceTodayCount }} -
-
-
+
- -
-
- 设备消息数 - -
- - {{ statsData.deviceMessageCount }} - - -
- 今日新增 - +{{ statsData.deviceMessageTodayCount }} -
-
-
+
- - -
-
+
- - - - -
-
- 在线设备 -
-
- -
-
- 离线设备 -
-
- -
-
- 待激活设备 -
-
-
-
+
- - -
-
+
@@ -148,356 +64,43 @@ - - diff --git a/src/views/iot/ota/firmware/OtaFirmwareForm.vue b/src/views/iot/ota/firmware/OtaFirmwareForm.vue new file mode 100644 index 00000000..9689a97a --- /dev/null +++ b/src/views/iot/ota/firmware/OtaFirmwareForm.vue @@ -0,0 +1,169 @@ + + diff --git a/src/views/iot/ota/firmware/detail/index.vue b/src/views/iot/ota/firmware/detail/index.vue new file mode 100644 index 00000000..00e75781 --- /dev/null +++ b/src/views/iot/ota/firmware/detail/index.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/views/iot/ota/firmware/index.vue b/src/views/iot/ota/firmware/index.vue new file mode 100644 index 00000000..bfc862b1 --- /dev/null +++ b/src/views/iot/ota/firmware/index.vue @@ -0,0 +1,232 @@ + + + diff --git a/src/views/iot/ota/task/OtaTaskDetail.vue b/src/views/iot/ota/task/OtaTaskDetail.vue new file mode 100644 index 00000000..950a3f97 --- /dev/null +++ b/src/views/iot/ota/task/OtaTaskDetail.vue @@ -0,0 +1,285 @@ + + + diff --git a/src/views/iot/ota/task/OtaTaskForm.vue b/src/views/iot/ota/task/OtaTaskForm.vue new file mode 100644 index 00000000..6fde972b --- /dev/null +++ b/src/views/iot/ota/task/OtaTaskForm.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/views/iot/ota/task/OtaTaskList.vue b/src/views/iot/ota/task/OtaTaskList.vue new file mode 100644 index 00000000..f6c3a6be --- /dev/null +++ b/src/views/iot/ota/task/OtaTaskList.vue @@ -0,0 +1,187 @@ + + + diff --git a/src/views/iot/plugin/PluginConfigForm.vue b/src/views/iot/plugin/PluginConfigForm.vue deleted file mode 100644 index b4f51029..00000000 --- a/src/views/iot/plugin/PluginConfigForm.vue +++ /dev/null @@ -1,106 +0,0 @@ - - diff --git a/src/views/iot/plugin/detail/PluginImportForm.vue b/src/views/iot/plugin/detail/PluginImportForm.vue deleted file mode 100644 index 2d3b7517..00000000 --- a/src/views/iot/plugin/detail/PluginImportForm.vue +++ /dev/null @@ -1,99 +0,0 @@ - - diff --git a/src/views/iot/plugin/detail/index.vue b/src/views/iot/plugin/detail/index.vue deleted file mode 100644 index 31ed167b..00000000 --- a/src/views/iot/plugin/detail/index.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - diff --git a/src/views/iot/plugin/index.vue b/src/views/iot/plugin/index.vue deleted file mode 100644 index a5d6545a..00000000 --- a/src/views/iot/plugin/index.vue +++ /dev/null @@ -1,329 +0,0 @@ - - - - diff --git a/src/views/iot/product/category/index.vue b/src/views/iot/product/category/index.vue index e7826221..f4210830 100644 --- a/src/views/iot/product/category/index.vue +++ b/src/views/iot/product/category/index.vue @@ -118,7 +118,6 @@ const queryParams = reactive({ createTime: [] }) const queryFormRef = ref() // 搜索的表单 -const exportLoading = ref(false) // 导出的加载中 /** 查询列表 */ const getList = async () => { diff --git a/src/views/iot/product/product/ProductForm.vue b/src/views/iot/product/product/ProductForm.vue index 83706fc6..5247e925 100644 --- a/src/views/iot/product/product/ProductForm.vue +++ b/src/views/iot/product/product/ProductForm.vue @@ -45,7 +45,7 @@ @@ -62,28 +62,10 @@ /> - - - - - - - + + @@ -91,10 +73,10 @@ - - + + @@ -124,14 +106,8 @@ diff --git a/src/views/iot/product/product/detail/ProductDetailsHeader.vue b/src/views/iot/product/product/detail/ProductDetailsHeader.vue index 841aef9f..91900647 100644 --- a/src/views/iot/product/product/detail/ProductDetailsHeader.vue +++ b/src/views/iot/product/product/detail/ProductDetailsHeader.vue @@ -13,7 +13,7 @@ 编辑 @@ -37,15 +37,13 @@
- + {{ product.productKey }} 复制 - - - - {{ product.deviceCount ?? '加载中...' }} + + {{ product.deviceCount ?? '加载中...' }} 前往管理 diff --git a/src/views/iot/product/product/detail/ProductDetailsInfo.vue b/src/views/iot/product/product/detail/ProductDetailsInfo.vue index cbcf3e7f..51ac544b 100644 --- a/src/views/iot/product/product/detail/ProductDetailsInfo.vue +++ b/src/views/iot/product/product/detail/ProductDetailsInfo.vue @@ -1,19 +1,19 @@ @@ -21,7 +16,6 @@ import { ProductApi, ProductVO } from '@/api/iot/product/product' import { DeviceApi } from '@/api/iot/device/device' import ProductDetailsHeader from './ProductDetailsHeader.vue' import ProductDetailsInfo from './ProductDetailsInfo.vue' -import ProductTopic from './ProductTopic.vue' import IoTProductThingModel from '@/views/iot/thingmodel/index.vue' import { useTagsViewStore } from '@/store/modules/tagsView' import { useRouter } from 'vue-router' diff --git a/src/views/iot/rule/data/index.vue b/src/views/iot/rule/data/index.vue new file mode 100644 index 00000000..8c887593 --- /dev/null +++ b/src/views/iot/rule/data/index.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/views/iot/rule/data/rule/DataRuleForm.vue b/src/views/iot/rule/data/rule/DataRuleForm.vue new file mode 100644 index 00000000..3adc171a --- /dev/null +++ b/src/views/iot/rule/data/rule/DataRuleForm.vue @@ -0,0 +1,158 @@ + + diff --git a/src/views/iot/rule/data/rule/components/SourceConfigForm.vue b/src/views/iot/rule/data/rule/components/SourceConfigForm.vue new file mode 100644 index 00000000..4e10138e --- /dev/null +++ b/src/views/iot/rule/data/rule/components/SourceConfigForm.vue @@ -0,0 +1,262 @@ + + + diff --git a/src/views/iot/rule/data/rule/index.vue b/src/views/iot/rule/data/rule/index.vue new file mode 100644 index 00000000..cce4830b --- /dev/null +++ b/src/views/iot/rule/data/rule/index.vue @@ -0,0 +1,196 @@ + + + diff --git a/src/views/iot/rule/databridge/IoTDataBridgeForm.vue b/src/views/iot/rule/data/sink/DataSinkForm.vue similarity index 64% rename from src/views/iot/rule/databridge/IoTDataBridgeForm.vue rename to src/views/iot/rule/data/sink/DataSinkForm.vue index 4e69c026..a4974575 100644 --- a/src/views/iot/rule/databridge/IoTDataBridgeForm.vue +++ b/src/views/iot/rule/data/sink/DataSinkForm.vue @@ -7,50 +7,41 @@ :rules="formRules" label-width="120px" > - - + + - - - - {{ dict.label }} - - + + - - - - {{ dict.label }} - - + + + + - - + + - - + - - - diff --git a/src/views/iot/rule/scene/form/configs/AlertConfig.vue b/src/views/iot/rule/scene/form/configs/AlertConfig.vue new file mode 100644 index 00000000..8073c351 --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/AlertConfig.vue @@ -0,0 +1,81 @@ + + + + diff --git a/src/views/iot/rule/scene/form/configs/ConditionConfig.vue b/src/views/iot/rule/scene/form/configs/ConditionConfig.vue new file mode 100644 index 00000000..7cb9bdb2 --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/ConditionConfig.vue @@ -0,0 +1,301 @@ + + + + + + diff --git a/src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue b/src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue new file mode 100644 index 00000000..9d304b70 --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue @@ -0,0 +1,234 @@ + + + + diff --git a/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue b/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue new file mode 100644 index 00000000..2cc89c93 --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue @@ -0,0 +1,376 @@ + + + + diff --git a/src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue b/src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue new file mode 100644 index 00000000..979eff89 --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue @@ -0,0 +1,251 @@ + + + + diff --git a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue new file mode 100644 index 00000000..4c61d31e --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue @@ -0,0 +1,340 @@ + + + diff --git a/src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue b/src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue new file mode 100644 index 00000000..3097fdc8 --- /dev/null +++ b/src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue @@ -0,0 +1,156 @@ + + + diff --git a/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue b/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue new file mode 100644 index 00000000..5bfa970d --- /dev/null +++ b/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue @@ -0,0 +1,519 @@ + + + + + + diff --git a/src/views/iot/rule/scene/form/inputs/ValueInput.vue b/src/views/iot/rule/scene/form/inputs/ValueInput.vue new file mode 100644 index 00000000..908141b3 --- /dev/null +++ b/src/views/iot/rule/scene/form/inputs/ValueInput.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/src/views/iot/rule/scene/form/sections/ActionSection.vue b/src/views/iot/rule/scene/form/sections/ActionSection.vue new file mode 100644 index 00000000..b41e8cf7 --- /dev/null +++ b/src/views/iot/rule/scene/form/sections/ActionSection.vue @@ -0,0 +1,272 @@ + + + + diff --git a/src/views/iot/rule/scene/form/sections/BasicInfoSection.vue b/src/views/iot/rule/scene/form/sections/BasicInfoSection.vue new file mode 100644 index 00000000..4e77053e --- /dev/null +++ b/src/views/iot/rule/scene/form/sections/BasicInfoSection.vue @@ -0,0 +1,86 @@ + + + + + + diff --git a/src/views/iot/rule/scene/form/sections/TriggerSection.vue b/src/views/iot/rule/scene/form/sections/TriggerSection.vue new file mode 100644 index 00000000..144d53c6 --- /dev/null +++ b/src/views/iot/rule/scene/form/sections/TriggerSection.vue @@ -0,0 +1,222 @@ + + + diff --git a/src/views/iot/rule/scene/form/selectors/DeviceSelector.vue b/src/views/iot/rule/scene/form/selectors/DeviceSelector.vue new file mode 100644 index 00000000..0aa9cdd9 --- /dev/null +++ b/src/views/iot/rule/scene/form/selectors/DeviceSelector.vue @@ -0,0 +1,103 @@ + + + + diff --git a/src/views/iot/rule/scene/form/selectors/OperatorSelector.vue b/src/views/iot/rule/scene/form/selectors/OperatorSelector.vue new file mode 100644 index 00000000..df98de66 --- /dev/null +++ b/src/views/iot/rule/scene/form/selectors/OperatorSelector.vue @@ -0,0 +1,264 @@ + + + + + + diff --git a/src/views/iot/rule/scene/form/selectors/ProductSelector.vue b/src/views/iot/rule/scene/form/selectors/ProductSelector.vue new file mode 100644 index 00000000..9162dcb5 --- /dev/null +++ b/src/views/iot/rule/scene/form/selectors/ProductSelector.vue @@ -0,0 +1,79 @@ + + + + diff --git a/src/views/iot/rule/scene/form/selectors/PropertySelector.vue b/src/views/iot/rule/scene/form/selectors/PropertySelector.vue new file mode 100644 index 00000000..51f2117a --- /dev/null +++ b/src/views/iot/rule/scene/form/selectors/PropertySelector.vue @@ -0,0 +1,437 @@ + + + + + + diff --git a/src/views/iot/rule/scene/index.vue b/src/views/iot/rule/scene/index.vue new file mode 100644 index 00000000..da4a7593 --- /dev/null +++ b/src/views/iot/rule/scene/index.vue @@ -0,0 +1,494 @@ + + + diff --git a/src/views/iot/thingmodel/ThingModelEvent.vue b/src/views/iot/thingmodel/ThingModelEvent.vue index 592eb86c..2704f4b5 100644 --- a/src/views/iot/thingmodel/ThingModelEvent.vue +++ b/src/views/iot/thingmodel/ThingModelEvent.vue @@ -6,21 +6,19 @@ prop="event.type" > - - {{ ThingModelEventType.INFO.label }} - - - {{ ThingModelEventType.ALERT.label }} - - - {{ ThingModelEventType.ERROR.label }} + + {{ eventType.label }} @@ -29,8 +27,11 @@ import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue' import { useVModel } from '@vueuse/core' import { ThingModelEvent } from '@/api/iot/thingmodel' -import { ThingModelEventType, ThingModelParamDirection } from './config' import { isEmpty } from '@/utils/is' +import { + IoTThingModelEventTypeEnum, + IoTThingModelParamDirectionEnum +} from '@/views/iot/utils/constants' /** IoT 物模型事件 */ defineOptions({ name: 'ThingModelEvent' }) @@ -42,7 +43,8 @@ const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref thingModelEvent.value.type, - (val: string) => isEmpty(val) && (thingModelEvent.value.type = ThingModelEventType.INFO.value), + (val: string) => + isEmpty(val) && (thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value), { immediate: true } ) diff --git a/src/views/iot/thingmodel/ThingModelForm.vue b/src/views/iot/thingmodel/ThingModelForm.vue index f52179bf..cc049fe1 100644 --- a/src/views/iot/thingmodel/ThingModelForm.vue +++ b/src/views/iot/thingmodel/ThingModelForm.vue @@ -27,16 +27,19 @@ - + ({ - type: ThingModelType.PROPERTY, - dataType: DataSpecsDataType.INT, + type: IoTThingModelTypeEnum.PROPERTY, + dataType: IoTDataSpecsDataTypeEnum.INT, property: { - dataType: DataSpecsDataType.INT, + dataType: IoTDataSpecsDataTypeEnum.INT, dataSpecs: { - dataType: DataSpecsDataType.INT + dataType: IoTDataSpecsDataTypeEnum.INT } }, service: {}, @@ -106,11 +112,11 @@ const open = async (type: string, id?: number) => { formData.value = await ThingModelApi.getThingModel(id) // 情况一:属性初始化 if (isEmpty(formData.value.property)) { - formData.value.dataType = DataSpecsDataType.INT + formData.value.dataType = IoTDataSpecsDataTypeEnum.INT formData.value.property = { - dataType: DataSpecsDataType.INT, + dataType: IoTDataSpecsDataTypeEnum.INT, dataSpecs: { - dataType: DataSpecsDataType.INT + dataType: IoTDataSpecsDataTypeEnum.INT } } } @@ -147,18 +153,18 @@ const submitForm = async () => { await ThingModelApi.updateThingModel(data) message.success(t('common.updateSuccess')) } - } finally { - dialogVisible.value = false // 确保关闭弹框 + // 关闭弹窗 + dialogVisible.value = false emit('success') + } finally { formLoading.value = false } } -/** 填写额外的属性 */ +/** 填写额外的属性(处理不同类型的情况) */ const fillExtraAttributes = (data: any) => { - // 处理不同类型的情况 // 属性 - if (data.type === ThingModelType.PROPERTY) { + if (data.type === IoTThingModelTypeEnum.PROPERTY) { removeDataSpecs(data.property) data.dataType = data.property.dataType data.property.identifier = data.identifier @@ -167,7 +173,7 @@ const fillExtraAttributes = (data: any) => { delete data.event } // 服务 - if (data.type === ThingModelType.SERVICE) { + if (data.type === IoTThingModelTypeEnum.SERVICE) { removeDataSpecs(data.service) data.dataType = data.service.dataType data.service.identifier = data.identifier @@ -176,7 +182,7 @@ const fillExtraAttributes = (data: any) => { delete data.event } // 事件 - if (data.type === ThingModelType.EVENT) { + if (data.type === IoTThingModelTypeEnum.EVENT) { removeDataSpecs(data.event) data.dataType = data.event.dataType data.event.identifier = data.identifier @@ -185,6 +191,7 @@ const fillExtraAttributes = (data: any) => { delete data.service } } + /** 处理 dataSpecs 为空的情况 */ const removeDataSpecs = (val: any) => { if (isEmpty(val.dataSpecs)) { @@ -198,12 +205,12 @@ const removeDataSpecs = (val: any) => { /** 重置表单 */ const resetForm = () => { formData.value = { - type: ThingModelType.PROPERTY, - dataType: DataSpecsDataType.INT, + type: IoTThingModelTypeEnum.PROPERTY, + dataType: IoTDataSpecsDataTypeEnum.INT, property: { - dataType: DataSpecsDataType.INT, + dataType: IoTDataSpecsDataTypeEnum.INT, dataSpecs: { - dataType: DataSpecsDataType.INT + dataType: IoTDataSpecsDataTypeEnum.INT } }, service: {}, diff --git a/src/views/iot/thingmodel/ThingModelInputOutputParam.vue b/src/views/iot/thingmodel/ThingModelInputOutputParam.vue index 2bf4bb93..04bc9509 100644 --- a/src/views/iot/thingmodel/ThingModelInputOutputParam.vue +++ b/src/views/iot/thingmodel/ThingModelInputOutputParam.vue @@ -15,7 +15,7 @@ +新增参数 - + -