Ver código fonte

Merge branch 'feature/iot' of https://gitee.com/yudaocode/yudao-ui-admin-vue3

# Conflicts:
#	.env
#	pnpm-lock.yaml
YunaiV 6 meses atrás
pai
commit
8922e261ab
100 arquivos alterados com 12046 adições e 2606 exclusões
  1. 4 1
      .env
  2. 2 1
      package.json
  3. 46 0
      src/api/iot/alert/config/index.ts
  4. 35 0
      src/api/iot/alert/record/index.ts
  5. 50 54
      src/api/iot/device/device/index.ts
  6. 44 0
      src/api/iot/ota/firmware/index.ts
  7. 38 0
      src/api/iot/ota/task/index.ts
  8. 38 0
      src/api/iot/ota/task/record/index.ts
  9. 0 51
      src/api/iot/plugin/index.ts
  10. 16 12
      src/api/iot/product/product/index.ts
  11. 39 0
      src/api/iot/rule/data/rule/index.ts
  12. 33 34
      src/api/iot/rule/databridge/index.ts
  13. 87 0
      src/api/iot/rule/scene/index.ts
  14. 28 9
      src/api/iot/statistics/index.ts
  15. 225 12
      src/api/iot/thingmodel/index.ts
  16. 1 11
      src/api/system/role/index.ts
  17. 3 0
      src/components/JsonEditor/index.ts
  18. 126 0
      src/components/JsonEditor/src/JsonEditor.vue
  19. 80 0
      src/components/JsonEditor/types/index.ts
  20. 268 0
      src/components/Map/index.vue
  21. 2 0
      src/plugins/echarts/index.ts
  22. 5 5
      src/router/modules/remaining.ts
  23. 471 0
      src/utils/cron.ts
  24. 11 9
      src/utils/dict.ts
  25. 201 0
      src/views/iot/alert/config/AlertConfigForm.vue
  26. 210 0
      src/views/iot/alert/config/index.vue
  27. 296 0
      src/views/iot/alert/record/index.vue
  28. 104 42
      src/views/iot/device/device/DeviceForm.vue
  29. 303 0
      src/views/iot/device/device/components/DeviceTableSelect.vue
  30. 0 110
      src/views/iot/device/device/detail/DeviceDataDetail.vue
  31. 42 27
      src/views/iot/device/device/detail/DeviceDetailConfig.vue
  32. 158 109
      src/views/iot/device/device/detail/DeviceDetailsInfo.vue
  33. 0 166
      src/views/iot/device/device/detail/DeviceDetailsLog.vue
  34. 201 0
      src/views/iot/device/device/detail/DeviceDetailsMessage.vue
  35. 0 134
      src/views/iot/device/device/detail/DeviceDetailsModel.vue
  36. 298 209
      src/views/iot/device/device/detail/DeviceDetailsSimulator.vue
  37. 35 0
      src/views/iot/device/device/detail/DeviceDetailsThingModel.vue
  38. 192 0
      src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue
  39. 245 0
      src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue
  40. 216 0
      src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue
  41. 208 0
      src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue
  42. 30 10
      src/views/iot/device/device/detail/index.vue
  43. 12 5
      src/views/iot/device/device/index.vue
  44. 50 0
      src/views/iot/home/components/ComparisonCard.vue
  45. 131 0
      src/views/iot/home/components/DeviceCountCard.vue
  46. 163 0
      src/views/iot/home/components/DeviceStateCountCard.vue
  47. 227 0
      src/views/iot/home/components/MessageTrendCard.vue
  48. 62 461
      src/views/iot/home/index.vue
  49. 169 0
      src/views/iot/ota/firmware/OtaFirmwareForm.vue
  50. 143 0
      src/views/iot/ota/firmware/detail/index.vue
  51. 232 0
      src/views/iot/ota/firmware/index.vue
  52. 285 0
      src/views/iot/ota/task/OtaTaskDetail.vue
  53. 132 0
      src/views/iot/ota/task/OtaTaskForm.vue
  54. 187 0
      src/views/iot/ota/task/OtaTaskList.vue
  55. 0 106
      src/views/iot/plugin/PluginConfigForm.vue
  56. 0 99
      src/views/iot/plugin/detail/PluginImportForm.vue
  57. 0 120
      src/views/iot/plugin/detail/index.vue
  58. 0 329
      src/views/iot/plugin/index.vue
  59. 0 1
      src/views/iot/product/category/index.vue
  60. 15 50
      src/views/iot/product/product/ProductForm.vue
  61. 220 0
      src/views/iot/product/product/components/ProductTableSelect.vue
  62. 4 6
      src/views/iot/product/product/detail/ProductDetailsHeader.vue
  63. 5 11
      src/views/iot/product/product/detail/ProductDetailsInfo.vue
  64. 0 247
      src/views/iot/product/product/detail/ProductTopic.vue
  65. 1 7
      src/views/iot/product/product/detail/index.vue
  66. 20 0
      src/views/iot/rule/data/index.vue
  67. 158 0
      src/views/iot/rule/data/rule/DataRuleForm.vue
  68. 262 0
      src/views/iot/rule/data/rule/components/SourceConfigForm.vue
  69. 196 0
      src/views/iot/rule/data/rule/index.vue
  70. 42 61
      src/views/iot/rule/databridge/IoTDataBridgeForm.vue
  71. 4 2
      src/views/iot/rule/databridge/config/HttpConfigForm.vue
  72. 2 2
      src/views/iot/rule/databridge/config/KafkaMQConfigForm.vue
  73. 2 2
      src/views/iot/rule/databridge/config/MqttConfigForm.vue
  74. 2 2
      src/views/iot/rule/databridge/config/RabbitMQConfigForm.vue
  75. 2 3
      src/views/iot/rule/databridge/config/RedisStreamMQConfigForm.vue
  76. 2 2
      src/views/iot/rule/databridge/config/RocketMQConfigForm.vue
  77. 0 1
      src/views/iot/rule/databridge/config/components/KeyValueEditor.vue
  78. 2 2
      src/views/iot/rule/databridge/config/index.ts
  79. 24 46
      src/views/iot/rule/databridge/index.vue
  80. 330 0
      src/views/iot/rule/scene/form/RuleSceneForm.vue
  81. 81 0
      src/views/iot/rule/scene/form/configs/AlertConfig.vue
  82. 301 0
      src/views/iot/rule/scene/form/configs/ConditionConfig.vue
  83. 234 0
      src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue
  84. 376 0
      src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue
  85. 251 0
      src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue
  86. 340 0
      src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue
  87. 156 0
      src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue
  88. 519 0
      src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue
  89. 310 0
      src/views/iot/rule/scene/form/inputs/ValueInput.vue
  90. 272 0
      src/views/iot/rule/scene/form/sections/ActionSection.vue
  91. 86 0
      src/views/iot/rule/scene/form/sections/BasicInfoSection.vue
  92. 222 0
      src/views/iot/rule/scene/form/sections/TriggerSection.vue
  93. 103 0
      src/views/iot/rule/scene/form/selectors/DeviceSelector.vue
  94. 264 0
      src/views/iot/rule/scene/form/selectors/OperatorSelector.vue
  95. 79 0
      src/views/iot/rule/scene/form/selectors/ProductSelector.vue
  96. 437 0
      src/views/iot/rule/scene/form/selectors/PropertySelector.vue
  97. 494 0
      src/views/iot/rule/scene/index.vue
  98. 13 11
      src/views/iot/thingmodel/ThingModelEvent.vue
  99. 31 24
      src/views/iot/thingmodel/ThingModelForm.vue
  100. 0 0
      src/views/iot/thingmodel/ThingModelInputOutputParam.vue

Diferenças do arquivo suprimidas por serem muito extensas
+ 4 - 1
.env


+ 2 - 1
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",

+ 46 - 0
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` })
+  }
+}

+ 35 - 0
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 }
+    })
+  }
+}

+ 50 - 54
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 // 更新时间
-  value: string // 最新值
+  dataSpecs: any // 数据定义
+  dataSpecsList: any[] // 数据定义列表
 }
 
-// IoT 设备数据 VO
-export interface DeviceHistoryDataVO {
-  time: number // 时间
-  data: string // 数据
+// IoT 设备属性 VO
+export interface IotDevicePropertyRespVO {
+  identifier: string // 属性标识符
+  value: string // 最新值
+  updateTime: Date // 更新时间
 }
 
+// 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 // 请求参数
-}
-
-// IoT 设备下行 Request VO
-export interface IotDeviceDownstreamReqVO {
-  id: number // 设备编号
-  type: string // 消息类型
-  identifier: string // 标识符
-  data: any // 请求参数
+// 设备认证参数 VO
+export interface IotDeviceAuthInfoVO {
+  clientId: string // 客户端 ID
+  username: string // 用户名
+  password: string // 密码
 }
 
-// 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 })
+  // 获取设备属性最新数据
+  getLatestDeviceProperties: async (params: any) => {
+    return await request.get({ url: `/iot/device/property/get-latest`, params })
   },
 
-  // 设备下行
-  downstreamDevice: async (data: IotDeviceDownstreamReqVO) => {
-    return await request.post({ url: `/iot/device/downstream`, data })
+  // 获取设备属性历史数据
+  getHistoryDevicePropertyList: async (params: any) => {
+    return await request.get({ url: `/iot/device/property/history-list`, params })
   },
 
-  // 获取设备属性最新数据
-  getLatestDeviceProperties: async (params: any) => {
-    return await request.get({ url: `/iot/device/property/latest`, params })
+  // 获取设备认证信息
+  getDeviceAuthInfo: async (id: number) => {
+    return await request.get({ url: `/iot/device/get-auth-info`, params: { id } })
   },
 
-  // 获取设备属性历史数据
-  getHistoryDevicePropertyPage: async (params: any) => {
-    return await request.get({ url: `/iot/device/property/history-page`, params })
+  // 查询设备消息分页
+  getDeviceMessagePage: async (params: any) => {
+    return await request.get({ url: `/iot/device/message/page`, params })
   },
 
-  // 查询设备日志分页
-  getDeviceLogPage: async (params: any) => {
-    return await request.get({ url: `/iot/device/log/page`, params })
+  // 查询设备消息配对分页
+  getDeviceMessagePairPage: async (params: any) => {
+    return await request.get({ url: `/iot/device/message/pair-page`, params })
   },
 
-  // 获取设备MQTT连接参数
-  getMqttConnectionParams: async (deviceId: number) => {
-    return await request.get({ url: `/iot/device/mqtt-connection-params`, params: { deviceId } })
+  // 发送设备消息
+  sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
+    return await request.post({ url: `/iot/device/message/send`, data: params })
   }
 }

+ 44 - 0
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 })
+  }
+}

+ 38 - 0
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 })
+  }
+}

+ 38 - 0
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 })
+  }
+}

+ 0 - 51
src/api/iot/plugin/index.ts

@@ -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 })
-  }
-}

+ 16 - 12
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 } })
   }
 }

+ 39 - 0
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 })
+  }
+}

+ 33 - 34
src/api/iot/rule/databridge/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' })
   }
 }

+ 87 - 0
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` })
+  }
+}

+ 28 - 9
src/api/iot/statistics/index.ts

@@ -16,25 +16,44 @@ export interface IotStatisticsSummaryRespVO {
   productCategoryDeviceCounts: Record<string, number>
 }
 
+/** 时间戳-数值的键值对类型 */
+interface TimeValueItem {
+  [key: string]: number
+}
+
 /** IoT 消息统计数据类型 */
 export interface IotStatisticsDeviceMessageSummaryRespVO {
-  upstreamCounts: Record<number, number>
-  downstreamCounts: Record<number, number>
+  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<IotStatisticsSummaryRespVO>({
       url: `/iot/statistics/get-summary`
     })
   },
 
-  // 查询设备上下行消息的数据统计
-  getIotStatisticsDeviceMessageSummary: async (params: { startTime: number; endTime: number }) => {
-    return await request.get<IotStatisticsDeviceMessageSummaryRespVO>({
-      url: `/iot/statistics/get-log-summary`,
+  // 获取设备消息的数据统计
+  getDeviceMessageSummaryByDate: async (params: IotStatisticsDeviceMessageReqVO) => {
+    return await request.get<IotStatisticsDeviceMessageSummaryByDateRespVO[]>({
+      url: `/iot/statistics/get-device-message-summary-by-date`,
       params
     })
   }

+ 225 - 12
src/api/iot/thingmodel/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { isEmpty } from '@/utils/is'
 
 /**
  * IoT 产品物模型
@@ -18,14 +19,6 @@ export interface ThingModelData {
 }
 
 /**
- * IoT 模拟设备
- */
-// TODO @super:和 ThingModelSimulatorData 会不会好点
-export interface SimulatorData extends ThingModelData {
-  simulateValue?: string | number // 用于存储模拟值 TODO @super:字段使用 value 会不会好点
-}
-
-/**
  * ThingModelProperty 类型
  */
 export interface 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()
+}

+ 1 - 11
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

+ 3 - 0
src/components/JsonEditor/index.ts

@@ -0,0 +1,3 @@
+import JsonEditor from './src/JsonEditor.vue'
+
+export { JsonEditor }

+ 126 - 0
src/components/JsonEditor/src/JsonEditor.vue

@@ -0,0 +1,126 @@
+<template>
+  <div ref="jsonEditorContainer" class="json-editor" :style="{ height }"></div>
+</template>
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import JSONEditor, { JSONEditorMode, JSONEditorOptions } from 'jsoneditor'
+import 'jsoneditor/dist/jsoneditor.min.css'
+import { JsonEditorEmits, JsonEditorExpose, JsonEditorProps } from '../types'
+
+/** 基于 https://github.com/josdejong/jsoneditor 二次封装组件,提供 JSON 编辑器功能。 */
+defineOptions({ name: 'JsonEditor' })
+
+const props = withDefaults(defineProps<JsonEditorProps>(), {
+  mode: 'view' as JSONEditorMode,
+  height: '400px',
+  showModeSelection: false,
+  showNavigationBar: false,
+  showStatusBar: false,
+  showMainMenuBar: true
+})
+
+const emits = defineEmits<JsonEditorEmits>()
+const jsonObj = useVModel(props, 'modelValue', emits) as Ref<any>
+const jsonEditorContainer = ref<HTMLElement | null>(null)
+let jsonEditor: JSONEditor | null = null
+
+// 设置默认值
+const height = props.height
+
+// 初始化JSONEditor
+const initJsonEditor = () => {
+  if (!jsonEditorContainer.value) return
+
+  // 合并默认配置和用户自定义配置
+  const options: JSONEditorOptions = {
+    mode: props.mode,
+    modes: props.showModeSelection
+      ? (['tree', 'code', 'form', 'text', 'view', 'preview'] as JSONEditorMode[])
+      : undefined,
+    navigationBar: props.showNavigationBar,
+    statusBar: props.showStatusBar,
+    mainMenuBar: props.showMainMenuBar,
+    onChange: () => {
+      jsonObj.value = jsonEditor?.get()
+      emits('change', jsonEditor?.get())
+    },
+    onValidationError: (errors: any) => {
+      emits('error', errors)
+    },
+    ...props.options
+  } as JSONEditorOptions
+
+  // 创建JSONEditor实例
+  jsonEditor = new JSONEditor(jsonEditorContainer.value, options)
+
+  // 设置初始值
+  if (jsonObj.value) {
+    jsonEditor.set(jsonObj.value)
+  }
+
+  if (props.mode === 'view') {
+    jsonEditor?.expandAll() // 默认展开全部
+  }
+}
+
+// 监听数据变化
+watch(
+  () => jsonObj.value,
+  (newValue) => {
+    if (!jsonEditor) return
+
+    try {
+      // 防止无限循环更新
+      const currentJson = jsonEditor.get()
+      if (JSON.stringify(currentJson) !== JSON.stringify(newValue)) {
+        jsonEditor.update(newValue)
+      }
+    } catch (error) {
+      console.error('JSON更新失败:', error)
+    }
+  },
+  { deep: true }
+)
+
+// 监听模式变化
+watch(
+  () => props.mode,
+  (newMode) => {
+    if (!jsonEditor) return
+    try {
+      jsonEditor.setMode(newMode)
+    } catch (error) {
+      console.error('切换模式失败:', error)
+    }
+  }
+)
+
+// 生命周期钩子
+onMounted(() => {
+  initJsonEditor()
+})
+
+onBeforeUnmount(() => {
+  if (jsonEditor) {
+    jsonEditor.destroy()
+    jsonEditor = null
+  }
+})
+
+// 暴露方法
+defineExpose<JsonEditorExpose>({
+  // 获取编辑器实例,以便可以调用更多JSONEditor的原生方法
+  getEditor: () => jsonEditor
+})
+</script>
+
+<style lang="scss" scoped>
+/* 隐藏 Ace 编辑器的 powered by ace 标记 */
+:deep(.jsoneditor-menu) {
+  /* 隐藏 powered by ace 标记 */
+  .jsoneditor-poweredBy {
+    display: none !important;
+  }
+}
+</style>

+ 80 - 0
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<JSONEditorOptions>
+}
+
+/**
+ * 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
+} 

+ 268 - 0
src/components/Map/index.vue

@@ -0,0 +1,268 @@
+<!-- 地图组件:基于百度地图GL实现 -->
+<!-- TODO @super:还存在两个没解决的小bug,一个是修改手动定位时一次加载 不知道为何定位点在地图左上角 调了半天没解决 第二个是检索地址确定定位的功能参照百度的文档没也搞好 回头再解决一下 -->
+<template>
+  <div v-if="props.isWrite">
+    <el-form ref="form" label-width="120px">
+      <el-form-item label="定位位置:">
+        <el-select
+          class="w-full"
+          v-model="state.address"
+          clearable
+          filterable
+          remote
+          reserve-keyword
+          placeholder="可输入地址查询经纬度"
+          :remote-method="autoSearch"
+          @change="handleAddressSelect"
+          :loading="state.loading"
+        >
+          <el-option
+            v-for="item in state.mapAddrOptions"
+            :key="item.value"
+            :label="item.name"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="设备地图:">
+        <!-- TODO @super:这里看看 unocss 哈 -->
+        <div id="bdMap" class="mapContainer"></div>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div v-else>
+    <el-descriptions :column="2" border :labelStyle="{ 'font-weight': 'bold' }">
+      <el-descriptions-item label="设备位置:">{{ state.address }}</el-descriptions-item>
+    </el-descriptions>
+    <div id="bdMap" class="mapContainer"></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, onMounted } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+// 扩展 Window 接口以包含百度地图 GL API
+declare global {
+  interface Window {
+    BMapGL: any
+    initBaiduMap: () => void
+  }
+}
+
+const emits = defineEmits(['locateChange', 'update:center'])
+const state = reactive({
+  lonLat: '', // 经度,纬度
+  address: '',
+  loading: false,
+  latitude: '', // 纬度
+  longitude: '', // 经度
+  map: null as any, // 地图对象
+  mapAddrOptions: [] as any[],
+  mapMarker: null as any, // 标记对象
+  geocoder: null as any,
+  autoComplete: null as any,
+  tips: [] // 搜索提示
+})
+
+const props = defineProps({
+  clickMap: propTypes.bool.def(false),
+  isWrite: propTypes.bool.def(false),
+  center: propTypes.string.def('')
+})
+
+/** 加载百度地图 */
+const loadMap = () => {
+  state.address = ''
+  state.latitude = ''
+  state.longitude = ''
+
+  // 创建百度地图 API 脚本,动态加载
+  const script = document.createElement('script')
+  script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${
+    import.meta.env.VITE_BAIDU_MAP_KEY
+  }&callback=initBaiduMap`
+  document.body.appendChild(script)
+
+  // 定义全局回调函数
+  window.initBaiduMap = () => {
+    initMap()
+    initGeocoder()
+    initAutoComplete()
+
+    // TODO @super:这里加一行注释
+    if (props.clickMap) {
+      state.map.addEventListener('click', (e: any) => {
+        console.log(e)
+        const point = e.latlng
+        console.log(point)
+        state.lonLat = point.lng + ',' + point.lat
+        console.log(state.lonLat)
+        regeoCode(state.lonLat)
+      })
+    }
+
+    // TODO @super:这里加一行注释
+    if (props.center) {
+      regeoCode(props.center)
+    }
+  }
+}
+
+/** 初始化地图 */
+const initMap = () => {
+  const mapId = 'bdMap'
+  state.map = new window.BMapGL.Map(mapId)
+  // TODO @super:这个是默认的哇?
+  state.map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11)
+  state.map.enableScrollWheelZoom()
+  state.map.disableDoubleClickZoom()
+
+  // 添加地图控件
+  state.map.addControl(new window.BMapGL.NavigationControl())
+  state.map.addControl(new window.BMapGL.ScaleControl())
+  state.map.addControl(new window.BMapGL.ZoomControl())
+}
+
+/** 初始化地理编码器 */
+const initGeocoder = () => {
+  state.geocoder = new window.BMapGL.Geocoder()
+}
+
+/** 初始化自动完成 */
+const initAutoComplete = () => {
+  state.autoComplete = new window.BMapGL.Autocomplete({
+    input: 'searchInput',
+    location: state.map
+  })
+}
+
+/**
+ * 搜索地址
+ * @param queryValue 搜索关键词
+ */
+const autoSearch = (queryValue: string) => {
+  if (!queryValue) {
+    state.mapAddrOptions = []
+    return
+  }
+
+  state.loading = true
+
+  // 使用百度地图地点检索服务
+  const localSearch = new window.BMapGL.LocalSearch(state.map, {
+    onSearchComplete: (results: any) => {
+      state.loading = false
+      const temp: any[] = []
+
+      if (results && results.getPoi) {
+        const pois = results.getPoi()
+        pois.forEach((p: any) => {
+          const point = p.point
+          if (point && point.lng && point.lat) {
+            temp.push({
+              name: p.title,
+              value: point.lng + ',' + point.lat
+            })
+          }
+        })
+      }
+
+      state.mapAddrOptions = temp
+    }
+  })
+
+  localSearch.search(queryValue)
+}
+
+/**
+ * 处理地址选择
+ * @param value 选中的地址值
+ */
+const handleAddressSelect = (value: string) => {
+  if (value) {
+    regeoCode(value)
+  }
+}
+
+/**
+ * 添加标记点
+ * @param lnglat 经纬度数组
+ */
+// TODO @super:拼写;尽量不要有 idea 绿色提醒哈
+const setMarker = (lnglat: any) => {
+  if (!lnglat) return
+
+  // 如果点标记已存在则先移除原点
+  if (state.mapMarker !== null) {
+    state.map.removeOverlay(state.mapMarker)
+    state.lonLat = ''
+  }
+
+  // 创建新的标记点
+  const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
+  state.mapMarker = new window.BMapGL.Marker(point)
+
+  // 添加点标记到地图
+  state.map.addOverlay(state.mapMarker)
+  state.map.centerAndZoom(point, 16)
+}
+
+/**
+ * 经纬度转化为地址、添加标记点
+ * @param lonLat 经度,纬度字符串
+ */
+// TODO @super:拼写;尽量不要有 idea 绿色提醒哈
+const regeoCode = (lonLat: string) => {
+  if (!lonLat) return
+
+  // TODO @super:拼写;尽量不要有 idea 绿色提醒哈
+  const lnglat = lonLat.split(',')
+  if (lnglat.length !== 2) return
+
+  state.longitude = lnglat[0]
+  state.latitude = lnglat[1]
+
+  // 通知父组件位置变更
+  emits('locateChange', lnglat)
+  emits('update:center', lonLat)
+
+  // 先将地图中心点设置到目标位置
+  const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
+  state.map.centerAndZoom(point, 16)
+
+  // 再设置标记并获取地址
+  setMarker(lnglat)
+  getAddress(lnglat)
+}
+
+// TODO @super:lnglat 拼写
+/**
+ * 根据经纬度获取地址信息
+ *
+ * @param lnglat 经纬度数组
+ */
+const getAddress = (lnglat: any) => {
+  const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
+
+  state.geocoder.getLocation(point, (result: any) => {
+    if (result && result.address) {
+      state.address = result.address
+    }
+  })
+}
+
+/** 显式暴露方法,使其可以被父组件访问 */
+defineExpose({ regeoCode })
+
+onMounted(() => {
+  loadMap()
+})
+</script>
+
+<style scoped>
+.mapContainer {
+  width: 100%;
+  height: 400px;
+}
+</style>

+ 2 - 0
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,

+ 5 - 5
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')
       }
     ]
   }

+ 471 - 0
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<string, number> // 名称映射,如月份名称
+}
+
+/** CRON 字段配置常量 */
+export const CRON_FIELD_CONFIGS: Record<CronFieldType, CronFieldConfig> = {
+  [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
+  }
+}

+ 11 - 9
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 记录状态
 }

+ 201 - 0
src/views/iot/alert/config/AlertConfigForm.vue

@@ -0,0 +1,201 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="140px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="配置名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入配置名称" />
+      </el-form-item>
+      <el-form-item label="配置描述" prop="description">
+        <el-input v-model="formData.description" placeholder="请输入配置描述" />
+      </el-form-item>
+      <el-form-item label="告警级别" prop="level">
+        <el-select v-model="formData.level" placeholder="请选择告警级别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_LEVEL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="配置状态" prop="status">
+        <el-select v-model="formData.status">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="关联场景联动规则" prop="sceneRuleIds">
+        <el-select
+          v-model="formData.sceneRuleIds"
+          multiple
+          placeholder="请选择关联的场景联动规则"
+          class="w-full"
+        >
+          <el-option
+            v-for="scene in sceneRuleOptions"
+            :key="scene.id"
+            :label="scene.name"
+            :value="scene.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="接收的用户" prop="receiveUserIds">
+        <el-select
+          v-model="formData.receiveUserIds"
+          multiple
+          placeholder="请选择接收的用户"
+          class="w-full"
+        >
+          <el-option
+            v-for="user in userOptions"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="接收类型" prop="receiveTypes">
+        <el-select
+          v-model="formData.receiveTypes"
+          multiple
+          placeholder="请选择接收类型"
+          class="w-full"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_RECEIVE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { RuleSceneApi } from '@/api/iot/rule/scene'
+import * as UserApi from '@/api/system/user'
+
+/** IoT 告警配置 表单 */
+defineOptions({ name: 'AlertConfigForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  level: undefined,
+  status: CommonStatusEnum.ENABLE,
+  sceneRuleIds: [],
+  receiveUserIds: [],
+  receiveTypes: []
+})
+const formRules = reactive({
+  name: [{ required: true, message: '配置名称不能为空', trigger: 'blur' }],
+  level: [{ required: true, message: '告警级别不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '配置状态不能为空', trigger: 'blur' }],
+  sceneRuleIds: [{ required: true, message: '关联场景联动规则不能为空', trigger: 'blur' }],
+  receiveUserIds: [{ required: true, message: '接收用户不能为空', trigger: 'blur' }],
+  receiveTypes: [{ required: true, message: '接收类型不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 选项数据
+const sceneRuleOptions = ref<any[]>([])
+const userOptions = ref<UserApi.UserVO[]>([])
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await AlertConfigApi.getAlertConfig(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  // 加载选项数据
+  await loadOptions()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 加载选项数据 */
+const loadOptions = async () => {
+  try {
+    // 加载场景联动规则选项
+    sceneRuleOptions.value = await RuleSceneApi.getSimpleRuleSceneList()
+    // 加载用户选项
+    userOptions.value = await UserApi.getSimpleUserList()
+  } catch (error) {
+    console.error('加载选项数据失败:', error)
+  }
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as AlertConfig
+    if (formType.value === 'create') {
+      await AlertConfigApi.createAlertConfig(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await AlertConfigApi.updateAlertConfig(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    level: undefined,
+    status: CommonStatusEnum.ENABLE,
+    sceneRuleIds: [],
+    receiveUserIds: [],
+    receiveTypes: []
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 210 - 0
src/views/iot/alert/config/index.vue

@@ -0,0 +1,210 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="配置名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入配置名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="配置状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择配置状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iot:alert-config:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      row-key="id"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+    >
+      <el-table-column label="配置编号" align="center" prop="id" />
+      <el-table-column label="配置名称" align="center" prop="name" />
+      <el-table-column label="配置描述" align="center" prop="description" />
+      <el-table-column label="告警级别" align="center" prop="level">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_ALERT_LEVEL" :value="scope.row.level" />
+        </template>
+      </el-table-column>
+      <el-table-column label="配置状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="关联场景联动规则" align="center" prop="sceneRuleIds" min-width="100">
+        <template #default="scope"> {{ scope.row.sceneRuleIds?.length || 0 }} 条 </template>
+      </el-table-column>
+      <el-table-column label="接收人" align="center" prop="receiveUserNames" />
+      <el-table-column label="接收类型" align="center" prop="receiveTypes">
+        <template #default="scope">
+          <dict-tag
+            v-for="(receiveType, index) in scope.row.receiveTypes"
+            :key="index"
+            :type="DICT_TYPE.IOT_ALERT_RECEIVE_TYPE"
+            :value="receiveType"
+            class="mr-1"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iot:alert-config:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iot:alert-config:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <AlertConfigForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
+import AlertConfigForm from './AlertConfigForm.vue'
+
+/** IoT 告警配置 列表 */
+defineOptions({ name: 'IotAlertConfig' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<AlertConfig[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await AlertConfigApi.getAlertConfigPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await AlertConfigApi.deleteAlertConfig(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 296 - 0
src/views/iot/alert/record/index.vue

@@ -0,0 +1,296 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="告警配置" prop="configId">
+        <el-select
+          v-model="queryParams.configId"
+          placeholder="请选择告警配置"
+          clearable
+          filterable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="config in alertConfigList"
+            :key="config.id"
+            :label="config.name"
+            :value="config.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="告警级别" prop="configLevel">
+        <el-select
+          v-model="queryParams.configLevel"
+          placeholder="请选择告警级别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_LEVEL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="产品" prop="productId">
+        <el-select
+          v-model="queryParams.productId"
+          placeholder="请选择产品"
+          clearable
+          filterable
+          @change="handleProductChange"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="product in productList"
+            :key="product.id"
+            :label="product.name"
+            :value="product.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="设备" prop="deviceId">
+        <el-select
+          v-model="queryParams.deviceId"
+          placeholder="请选择设备"
+          clearable
+          filterable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="device in filteredDeviceList"
+            :key="device.id"
+            :label="device.deviceName"
+            :value="device.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否处理" prop="processStatus">
+        <el-select
+          v-model="queryParams.processStatus"
+          placeholder="请选择是否处理"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="String(dict.value)"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      row-key="id"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+    >
+      <el-table-column label="记录编号" align="center" prop="id" />
+      <el-table-column label="告警名称" align="center" prop="configName" />
+      <el-table-column label="告警级别" align="center" prop="configLevel">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_ALERT_LEVEL" :value="scope.row.configLevel" />
+        </template>
+      </el-table-column>
+      <el-table-column label="产品名称" align="center" prop="productId">
+        <template #default="scope">
+          {{ getProductName(scope.row.productId) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="设备名称" align="center" prop="deviceId">
+        <template #default="scope">
+          {{ getDeviceName(scope.row.deviceId) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="触发的设备消息" align="center" prop="deviceMessage">
+        <template #default="scope">
+          <el-popover
+            placement="top-start"
+            :width="600"
+            trigger="hover"
+            v-if="scope.row.deviceMessage"
+          >
+            <template #reference>
+              <el-button link type="primary">
+                <Icon icon="ep:view" class="mr-5px" />
+                查看消息
+              </el-button>
+            </template>
+            <pre>{{ scope.row.deviceMessage }}</pre>
+          </el-popover>
+          <span v-else class="text-gray-400">-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否处理" align="center" prop="processStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.processStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="处理结果" align="center" prop="processRemark" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            v-if="!scope.row.processStatus"
+            link
+            type="primary"
+            @click="handleProcess(scope.row)"
+            v-hasPermi="['iot:alert-record:process']"
+          >
+            处理
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { AlertRecordApi, AlertRecord } from '@/api/iot/alert/record'
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { DICT_TYPE, getIntDictOptions, getBoolDictOptions } from '@/utils/dict'
+
+/** IoT 告警记录列表 */
+defineOptions({ name: 'IotAlertRecord' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const list = ref<AlertRecord[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const alertConfigList = ref<AlertConfig[]>([]) // 告警配置列表
+const productList = ref<ProductVO[]>([]) // 产品列表
+const deviceList = ref<DeviceVO[]>([]) // 设备列表
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  configId: undefined as number | undefined,
+  configLevel: undefined as number | undefined,
+  productId: undefined as number | undefined,
+  deviceId: undefined as number | undefined,
+  processStatus: undefined as boolean | undefined,
+  createTime: [] as string[]
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 根据选择的产品 ID,筛选设备列表 */
+const filteredDeviceList = computed(() => {
+  if (!queryParams.productId) {
+    return deviceList.value
+  }
+  return deviceList.value.filter((device) => device.productId === queryParams.productId)
+})
+
+/** 根据产品 ID 获取产品名称 */
+const getProductName = (productId: number) => {
+  if (!productId) {
+    return `-`
+  }
+  const product = productList.value.find((p) => p.id === productId)
+  return product ? product.name : `加载中...`
+}
+
+/** 根据设备 ID 获取设备名称 */
+const getDeviceName = (deviceId: number) => {
+  if (!deviceId) {
+    return `-`
+  }
+  const device = deviceList.value.find((d) => d.id === deviceId)
+  return device ? device.deviceName : `加载中...`
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await AlertRecordApi.getAlertRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 产品变更处理 */
+const handleProductChange = () => {
+  queryParams.deviceId = undefined // 清空设备选择
+}
+
+/** 处理告警记录 */
+const handleProcess = async (row: AlertRecord) => {
+  try {
+    const { value: processRemark } = await ElMessageBox.prompt('请输入处理原因', '处理告警记录', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消'
+    })
+    await AlertRecordApi.processAlertRecord(row.id, processRemark)
+    message.success('处理成功')
+    await getList()
+  } catch (error) {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  alertConfigList.value = await AlertConfigApi.getSimpleAlertConfigList()
+  productList.value = await ProductApi.getSimpleProductList()
+  deviceList.value = await DeviceApi.getSimpleDeviceList()
+})
+</script>

+ 104 - 42
src/views/iot/device/device/DeviceForm.vue

@@ -23,19 +23,6 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="DeviceKey" prop="deviceKey">
-        <el-input
-          v-model="formData.deviceKey"
-          placeholder="请输入 DeviceKey"
-          :disabled="formType === 'update'"
-        >
-          <template #append>
-            <el-button @click="generateDeviceKey" :disabled="formType === 'update'">
-              重新生成
-            </el-button>
-          </template>
-        </el-input>
-      </el-form-item>
       <el-form-item label="DeviceName" prop="deviceName">
         <el-input
           v-model="formData.deviceName"
@@ -79,6 +66,44 @@
           <el-form-item label="设备序列号" prop="serialNumber">
             <el-input v-model="formData.serialNumber" placeholder="请输入设备序列号" />
           </el-form-item>
+          <el-form-item label="定位类型" prop="locationType">
+            <el-radio-group v-model="formData.locationType">
+              <el-radio
+                v-for="dict in getIntDictOptions(DICT_TYPE.IOT_LOCATION_TYPE)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <!-- LocationTypeEnum.MANUAL:手动定位 -->
+          <template v-if="LocationTypeEnum.MANUAL === formData.locationType">
+            <el-form-item label="设备经度" prop="longitude" type="number">
+              <el-input
+                v-model="formData.longitude"
+                placeholder="请输入设备经度"
+                @blur="updateLocationFromCoordinates"
+              />
+            </el-form-item>
+            <el-form-item label="设备维度" prop="latitude" type="number">
+              <el-input
+                v-model="formData.latitude"
+                placeholder="请输入设备维度"
+                @blur="updateLocationFromCoordinates"
+              />
+            </el-form-item>
+            <div class="pl-0 h-[400px] w-full ml-[-18px]" v-if="showMap">
+              <Map
+                :isWrite="true"
+                :clickMap="true"
+                :center="formData.location"
+                @locate-change="handleLocationChange"
+                ref="mapRef"
+                class="h-full w-full"
+              />
+            </div>
+          </template>
         </el-collapse-item>
       </el-collapse>
     </el-form>
@@ -91,9 +116,11 @@
 <script setup lang="ts">
 import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
 import { DeviceGroupApi } from '@/api/iot/device/group'
-import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceTypeEnum, LocationTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
 import { UploadImg } from '@/components/UploadFile'
-import { generateRandomStr } from '@/utils'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import Map from '@/components/Map/index.vue'
+import { ref } from 'vue'
 
 /** IoT 设备表单 */
 defineOptions({ name: 'IoTDeviceForm' })
@@ -105,28 +132,36 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const showMap = ref(false) // 是否显示地图组件
+const mapRef = ref(null)
+
 const formData = ref({
   id: undefined,
   productId: undefined,
-  deviceKey: undefined as string | undefined,
   deviceName: undefined,
   nickname: undefined,
   picUrl: undefined,
   gatewayId: undefined,
   deviceType: undefined as number | undefined,
   serialNumber: undefined,
+  locationType: undefined as number | undefined,
+  longitude: undefined,
+  latitude: undefined,
+  location: '', // 格式: "经度,纬度"
   groupIds: [] as number[]
 })
+
+/** 监听经纬度变化,更新location */
+watch([() => formData.value.longitude, () => formData.value.latitude], ([newLong, newLat]) => {
+  if (newLong && newLat) {
+    formData.value.location = `${newLong},${newLat}`
+    // 有了经纬度数据后显示地图
+    showMap.value = true
+  }
+})
+
 const formRules = reactive({
   productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
-  deviceKey: [
-    { required: true, message: 'DeviceKey 不能为空', trigger: 'blur' },
-    {
-      pattern: /^[a-zA-Z0-9]+$/,
-      message: 'DeviceKey 只能包含字母和数字',
-      trigger: 'blur'
-    }
-  ],
   deviceName: [
     { required: true, message: 'DeviceName 不能为空', trigger: 'blur' },
     {
@@ -138,7 +173,7 @@ const formRules = reactive({
   ],
   nickname: [
     {
-      validator: (rule, value, callback) => {
+      validator: (_rule, value: any, callback) => {
         if (value === undefined || value === null) {
           callback()
           return
@@ -175,33 +210,32 @@ const open = async (type: string, id?: number) => {
   formType.value = type
   resetForm()
 
+  // 默认不显示地图,等待数据加载
+  showMap.value = false
+
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
     try {
       formData.value = await DeviceApi.getDevice(id)
+
+      // 如果有经纬度,设置 location 字段用于地图显示
+      if (formData.value.longitude && formData.value.latitude) {
+        formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
+      }
     } finally {
       formLoading.value = false
     }
-  } else {
-    generateDeviceKey()
   }
+  // 如果有经纬信息,则数据加载完成后,显示地图
+  showMap.value = true
 
   // 加载网关设备列表
-  try {
-    gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY)
-  } catch (error) {
-    console.error('加载网关设备列表失败:', error)
-  }
+  gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY)
   // 加载产品列表
   products.value = await ProductApi.getSimpleProductList()
-
   // 加载设备分组列表
-  try {
-    deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
-  } catch (error) {
-    console.error('加载设备分组列表失败:', error)
-  }
+  deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -214,6 +248,16 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     const data = formData.value as unknown as DeviceVO
+    // 如果非手动定位,不进行提交该字段
+    if (data.locationType !== LocationTypeEnum.MANUAL) {
+      data.longitude = undefined
+      data.latitude = undefined
+    }
+    // TODO @宗超:【设备定位】address 和 areaId 也要处理;
+    // 1. 手动定位时:longitude + latitude + areaId + address:要稍微注意,address 可能要去掉省市区部分?!
+    // 2. IP 定位时:IotDeviceMessage 的 buildStateUpdateOnline 时,增加 ip 字段。这样,解析到 areaId;另外看看能不能通过 https://lbsyun.baidu.com/faq/api?title=webapi/ip-api-base(只获取 location 就 ok 啦)
+    // 3. 设备定位时:问问 haohao,一般怎么做。
+
     if (formType.value === 'create') {
       await DeviceApi.createDevice(data)
       message.success(t('common.createSuccess'))
@@ -234,16 +278,22 @@ const resetForm = () => {
   formData.value = {
     id: undefined,
     productId: undefined,
-    deviceKey: undefined,
     deviceName: undefined,
     nickname: undefined,
     picUrl: undefined,
     gatewayId: undefined,
     deviceType: undefined,
     serialNumber: undefined,
+    locationType: undefined,
+    longitude: undefined,
+    latitude: undefined,
+    // TODO @宗超:【设备定位】location 是不是拿出来,不放在 formData 里
+    location: '',
     groupIds: []
   }
   formRef.value?.resetFields()
+  // 重置表单时,隐藏地图
+  showMap.value = false
 }
 
 /** 产品选择变化 */
@@ -254,10 +304,22 @@ const handleProductChange = (productId: number) => {
   }
   const product = products.value?.find((item) => item.id === productId)
   formData.value.deviceType = product?.deviceType
+  formData.value.locationType = product?.locationType
 }
 
-/** 生成 DeviceKey */
-const generateDeviceKey = () => {
-  formData.value.deviceKey = generateRandomStr(16)
+/** 处理位置变化 */
+const handleLocationChange = (lnglat) => {
+  formData.value.longitude = lnglat[0]
+  formData.value.latitude = lnglat[1]
+}
+
+/** 根据经纬度更新地图位置 */
+const updateLocationFromCoordinates = () => {
+  // 验证经纬度是否有效
+  if (formData.value.longitude && formData.value.latitude) {
+    // 更新 location 字段,地图组件会根据此字段更新
+    formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
+    mapRef.value.regeoCode(formData.value.location)
+  }
 }
 </script>

+ 303 - 0
src/views/iot/device/device/components/DeviceTableSelect.vue

@@ -0,0 +1,303 @@
+<!-- IoT 设备选择,使用弹窗展示 -->
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
+    <ContentWrap>
+      <!-- 搜索工作栏 -->
+      <el-form
+        ref="queryFormRef"
+        :inline="true"
+        :model="queryParams"
+        class="-mb-15px"
+        label-width="100px"
+      >
+        <el-form-item v-if="!props.productId" label="产品" prop="productId">
+          <el-select
+            v-model="queryParams.productId"
+            placeholder="请选择产品"
+            clearable
+            class="!w-240px"
+          >
+            <el-option
+              v-for="product in products"
+              :key="product.id"
+              :label="product.name"
+              :value="product.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="DeviceName" prop="deviceName">
+          <el-input
+            v-model="queryParams.deviceName"
+            placeholder="请输入 DeviceName"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="备注名称" prop="nickname">
+          <el-input
+            v-model="queryParams.nickname"
+            placeholder="请输入备注名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="设备类型" prop="deviceType">
+          <el-select
+            v-model="queryParams.deviceType"
+            placeholder="请选择设备类型"
+            clearable
+            class="!w-240px"
+          >
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设备状态" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            placeholder="请选择设备状态"
+            clearable
+            class="!w-240px"
+          >
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设备分组" prop="groupId">
+          <el-select
+            v-model="queryParams.groupId"
+            placeholder="请选择设备分组"
+            clearable
+            class="!w-240px"
+          >
+            <el-option
+              v-for="group in deviceGroups"
+              :key="group.id"
+              :label="group.name"
+              :value="group.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon class="mr-5px" icon="ep:search" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon class="mr-5px" icon="ep:refresh" />
+            重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="list"
+        :show-overflow-tooltip="true"
+        :stripe="true"
+        @row-click="handleRowClick"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column v-if="multiple" type="selection" width="55" />
+        <el-table-column v-else width="55">
+          <template #default="scope">
+            <el-radio
+              v-model="selectedId"
+              :value="scope.row.id"
+              @change="() => handleRadioChange(scope.row)"
+            >
+              &nbsp;
+            </el-radio>
+          </template>
+        </el-table-column>
+        <el-table-column label="DeviceName" align="center" prop="deviceName" />
+        <el-table-column label="备注名称" align="center" prop="nickname" />
+        <el-table-column label="所属产品" align="center" prop="productId">
+          <template #default="scope">
+            {{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="设备类型" align="center" prop="deviceType">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="所属分组" align="center" prop="groupId">
+          <template #default="scope">
+            <template v-if="scope.row.groupIds?.length">
+              <el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
+                {{ deviceGroups.find((g) => g.id === id)?.name }}
+              </el-tag>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column label="设备状态" align="center" prop="status">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="最后上线时间"
+          align="center"
+          prop="onlineTime"
+          :formatter="dateFormatter"
+          width="180px"
+        />
+      </el-table>
+
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
+        @pagination="getList"
+      />
+    </ContentWrap>
+
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
+
+defineOptions({ name: 'IoTDeviceTableSelect' })
+
+const props = defineProps({
+  multiple: {
+    type: Boolean,
+    default: false
+  },
+  productId: {
+    type: Number,
+    default: null
+  }
+})
+
+const message = useMessage()
+const dialogVisible = ref(false)
+const dialogTitle = ref('设备选择器')
+const formLoading = ref(false)
+const loading = ref(true) // 列表的加载中
+const list = ref<DeviceVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const selectedDevices = ref<DeviceVO[]>([]) // 选中的设备列表
+const selectedId = ref<number>() // 单选模式下选中的ID
+const products = ref<ProductVO[]>([]) // 产品列表
+const deviceGroups = ref<DeviceGroupVO[]>([]) // 设备分组列表
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deviceName: undefined,
+  productId: undefined,
+  deviceType: undefined,
+  nickname: undefined,
+  status: undefined,
+  groupId: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    if (props.productId) {
+      queryParams.productId = props.productId as unknown as any
+    }
+    const data = await DeviceApi.getDevicePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  // 重置选择状态
+  selectedDevices.value = []
+  selectedId.value = undefined
+  if (!props.productId) {
+    // 获取产品列表
+    products.value = await ProductApi.getSimpleProductList()
+  }
+  // 获取设备列表
+  await getList()
+}
+defineExpose({ open })
+
+/** 处理行点击事件 */
+const tableRef = ref()
+const handleRowClick = (row: DeviceVO) => {
+  if (props.multiple) {
+    tableRef.value?.toggleRowSelection(row)
+  } else {
+    selectedId.value = row.id
+    selectedDevices.value = [row]
+  }
+}
+
+/** 处理单选变更事件 */
+const handleRadioChange = (row: DeviceVO) => {
+  selectedDevices.value = [row]
+}
+
+/** 处理选择变更事件 */
+const handleSelectionChange = (selection: DeviceVO[]) => {
+  if (props.multiple) {
+    selectedDevices.value = selection
+  }
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  if (selectedDevices.value.length === 0) {
+    message.warning(props.multiple ? '请至少选择一个设备' : '请选择一个设备')
+    return
+  }
+  emit('success', props.multiple ? selectedDevices.value : selectedDevices.value[0])
+  dialogVisible.value = false
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  // 获取分组列表
+  deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
+})
+</script>

+ 0 - 110
src/views/iot/device/device/detail/DeviceDataDetail.vue

@@ -1,110 +0,0 @@
-<!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)-->
-<template>
-  <Dialog title="查看数据" v-model="dialogVisible">
-    <ContentWrap>
-      <!-- 搜索工作栏 -->
-      <el-form
-        class="-mb-15px"
-        :model="queryParams"
-        ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-      >
-        <el-form-item label="时间" prop="createTime">
-          <el-date-picker
-            v-model="queryParams.times"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            type="datetimerange"
-            start-placeholder="开始日期"
-            end-placeholder="结束日期"
-            class="!w-350px"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery">
-            <Icon icon="ep:search" class="mr-5px" />
-            搜索
-          </el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
-
-    <!-- TODO @haohao:可参考阿里云 IoT,改成“图标”、“表格”两个选项 -->
-    <!-- 列表 -->
-    <ContentWrap>
-      <el-table v-loading="detailLoading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column
-          label="时间"
-          align="center"
-          prop="updateTime"
-          :formatter="dateFormatter"
-          width="180px"
-        />
-        <el-table-column label="属性值" align="center" prop="value" />
-      </el-table>
-      <!-- 分页 -->
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </ContentWrap>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import { DeviceApi, DeviceHistoryDataVO, DeviceVO } from '@/api/iot/device/device'
-import { ProductVO } from '@/api/iot/product/product'
-import { beginOfDay, dateFormatter, endOfDay, formatDate } from '@/utils/formatTime'
-
-defineProps<{ product: ProductVO; device: DeviceVO }>()
-
-/** IoT 设备数据详情 */
-defineOptions({ name: 'IoTDeviceDataDetail' })
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const detailLoading = ref(false)
-
-const list = ref<DeviceHistoryDataVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  deviceId: -1,
-  identifier: '',
-  times: [
-    // 默认显示最近一周的数据
-    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
-    formatDate(endOfDay(new Date()))
-  ]
-})
-const queryFormRef = ref() // 搜索的表单
-
-/** 获得设备历史数据 */
-const getList = async () => {
-  detailLoading.value = true
-  try {
-    const data = await DeviceApi.getHistoryDevicePropertyPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    detailLoading.value = false
-  }
-}
-
-/** 打开弹窗 */
-const open = (deviceId: number, identifier: string) => {
-  dialogVisible.value = true
-  queryParams.deviceId = deviceId
-  queryParams.identifier = identifier
-  getList()
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>

+ 42 - 27
src/views/iot/device/device/detail/DeviceDetailConfig.vue

@@ -8,24 +8,10 @@
       class="my-4"
       description="如需编辑文件,请点击下方编辑按钮"
     />
-
-    <!-- JSON 编辑器:读模式 -->
-    <Vue3Jsoneditor
-      v-if="isEditing"
+    <JsonEditor
       v-model="config"
-      :options="editorOptions"
-      height="500px"
-      currentMode="code"
-      @error="onError"
-    />
-    <!-- JSON 编辑器:写模式 -->
-    <Vue3Jsoneditor
-      v-else
-      v-model="config"
-      :options="editorOptions"
-      height="500px"
-      currentMode="view"
-      v-loading.fullscreen.lock="loading"
+      :mode="isEditing ? 'code' : 'view'"
+      height="600px"
       @error="onError"
     />
     <div class="mt-5 text-center">
@@ -34,15 +20,20 @@
         保存
       </el-button>
       <el-button v-else @click="enableEdit">编辑</el-button>
-      <!-- TODO @芋艿:缺一个下发按钮 -->
+      <el-button v-if="!isEditing" type="success" @click="handleConfigPush" :loading="pushLoading">
+        配置推送
+      </el-button>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import Vue3Jsoneditor from 'v3-jsoneditor/src/Vue3Jsoneditor.vue'
 import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
 import { jsonParse } from '@/utils'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'DeviceDetailConfig' })
 
 const props = defineProps<{
   device: DeviceVO
@@ -54,6 +45,7 @@ const emit = defineEmits<{
 
 const message = useMessage()
 const loading = ref(false) // 加载中
+const pushLoading = ref(false) // 推送加载中
 const config = ref<any>({}) // 只存储 config 字段
 const hasJsonError = ref(false) // 是否有 JSON 格式错误
 
@@ -63,12 +55,6 @@ watchEffect(() => {
 })
 
 const isEditing = ref(false) // 编辑状态
-const editorOptions = computed(() => ({
-  mainMenuBar: false,
-  navigationBar: false,
-  statusBar: false
-})) // JSON 编辑器的选项
-
 /** 启用编辑模式的函数 */
 const enableEdit = () => {
   isEditing.value = true
@@ -92,6 +78,32 @@ const saveConfig = async () => {
   isEditing.value = false
 }
 
+/** 配置推送处理函数 */
+const handleConfigPush = async () => {
+  try {
+    // 二次确认
+    await message.confirm('确定要推送配置到设备吗?此操作将远程更新设备配置。', '配置推送确认')
+
+    pushLoading.value = true
+
+    // 调用配置推送接口
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
+      params: config.value
+    })
+
+    message.success('配置推送成功!')
+  } catch (error) {
+    if (error !== 'cancel') {
+      message.error('配置推送失败!')
+      console.error('配置推送错误:', error)
+    }
+  } finally {
+    pushLoading.value = false
+  }
+}
+
 /** 更新设备配置 */
 const updateDeviceConfig = async () => {
   try {
@@ -112,8 +124,11 @@ const updateDeviceConfig = async () => {
 }
 
 /** 处理 JSON 编辑器错误的函数 */
-const onError = (e: any) => {
-  console.log('onError', e)
+const onError = (errors: any) => {
+  if (isEmpty(errors)) {
+    hasJsonError.value = false
+    return
+  }
   hasJsonError.value = true
 }
 </script>

+ 158 - 109
src/views/iot/device/device/detail/DeviceDetailsInfo.vue

@@ -1,111 +1,168 @@
 <!-- 设备信息 -->
 <template>
-  <ContentWrap>
-    <el-descriptions :column="3" title="设备信息">
-      <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
-      <el-descriptions-item label="ProductKey">
-        {{ product.productKey }}
-        <el-button @click="copyToClipboard(product.productKey)">复制</el-button>
-      </el-descriptions-item>
-      <el-descriptions-item label="设备类型">
-        <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
-      </el-descriptions-item>
-      <el-descriptions-item label="DeviceName">
-        {{ device.deviceName }}
-        <el-button @click="copyToClipboard(device.deviceName)">复制</el-button>
-      </el-descriptions-item>
-      <el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
-      <el-descriptions-item label="创建时间">
-        {{ formatDate(device.createTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="当前状态">
-        <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
-      </el-descriptions-item>
-      <el-descriptions-item label="激活时间">
-        {{ formatDate(device.activeTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="最后上线时间">
-        {{ formatDate(device.onlineTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="最后离线时间" :span="3">
-        {{ formatDate(device.offlineTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="MQTT 连接参数">
-        <el-button type="primary" @click="openMqttParams">查看</el-button>
-      </el-descriptions-item>
-    </el-descriptions>
-  </ContentWrap>
+  <div>
+    <ContentWrap>
+      <el-row :gutter="16">
+        <!-- 左侧设备信息 -->
+        <el-col :span="12">
+          <el-card class="h-full">
+            <template #header>
+              <div class="flex items-center">
+                <Icon icon="ep:info-filled" class="mr-2 text-primary" />
+                <span>设备信息</span>
+              </div>
+            </template>
+            <el-descriptions :column="2" border>
+              <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
+              <el-descriptions-item label="ProductKey">
+                {{ product.productKey }}
+              </el-descriptions-item>
+              <el-descriptions-item label="设备类型">
+                <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
+              </el-descriptions-item>
+              <el-descriptions-item label="定位类型">
+                <dict-tag :type="DICT_TYPE.IOT_LOCATION_TYPE" :value="device.locationType" />
+              </el-descriptions-item>
+              <el-descriptions-item label="DeviceName">
+                {{ device.deviceName }}
+              </el-descriptions-item>
+              <el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
+              <el-descriptions-item label="创建时间">
+                {{ formatDate(device.createTime) }}
+              </el-descriptions-item>
+              <el-descriptions-item label="当前状态">
+                <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
+              </el-descriptions-item>
+              <el-descriptions-item label="激活时间">
+                {{ formatDate(device.activeTime) }}
+              </el-descriptions-item>
+              <el-descriptions-item label="最后上线时间">
+                {{ formatDate(device.onlineTime) }}
+              </el-descriptions-item>
+              <el-descriptions-item label="最后离线时间">
+                {{ formatDate(device.offlineTime) }}
+              </el-descriptions-item>
+              <el-descriptions-item label="认证信息">
+                <el-button type="primary" @click="handleAuthInfoDialogOpen" plain size="small"
+                  >查看</el-button
+                >
+              </el-descriptions-item>
+            </el-descriptions>
+          </el-card>
+        </el-col>
 
-  <!-- MQTT 连接参数弹框 -->
-  <Dialog
-    title="MQTT 连接参数"
-    v-model="mqttDialogVisible"
-    width="50%"
-    :before-close="handleCloseMqttDialog"
-  >
-    <el-form :model="mqttParams" label-width="120px">
-      <el-form-item label="clientId">
-        <el-input v-model="mqttParams.mqttClientId" readonly>
-          <template #append>
-            <el-button @click="copyToClipboard(mqttParams.mqttClientId)" type="primary">
-              <Icon icon="ph:copy" />
-            </el-button>
-          </template>
-        </el-input>
-      </el-form-item>
-      <el-form-item label="username">
-        <el-input v-model="mqttParams.mqttUsername" readonly>
-          <template #append>
-            <el-button @click="copyToClipboard(mqttParams.mqttUsername)" type="primary">
-              <Icon icon="ph:copy" />
-            </el-button>
-          </template>
-        </el-input>
-      </el-form-item>
-      <el-form-item label="passwd">
-        <el-input
-          v-model="mqttParams.mqttPassword"
-          readonly
-          :type="passwordVisible ? 'text' : 'password'"
-        >
-          <template #append>
-            <el-button @click="passwordVisible = !passwordVisible" type="primary">
-              <Icon :icon="passwordVisible ? 'ph:eye-slash' : 'ph:eye'" />
-            </el-button>
-            <el-button @click="copyToClipboard(mqttParams.mqttPassword)" type="primary">
-              <Icon icon="ph:copy" />
-            </el-button>
-          </template>
-        </el-input>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="mqttDialogVisible = false">关闭</el-button>
-    </template>
-  </Dialog>
+        <!-- 右侧地图 -->
+        <el-col :span="12">
+          <el-card class="h-full">
+            <template #header>
+              <div class="flex items-center justify-between">
+                <div class="flex items-center">
+                  <Icon icon="ep:location" class="mr-2 text-primary" />
+                  <span>设备位置</span>
+                </div>
+                <div class="text-[14px] text-[var(--el-text-color-secondary)]">
+                  最后上线时间:
+                  {{ device.onlineTime ? formatDate(device.onlineTime) : '--' }}
+                </div>
+              </div>
+            </template>
+            <div class="h-[400px] w-full">
+              <Map v-if="showMap" :center="getLocationString()" class="h-full w-full" />
+              <div
+                v-else
+                class="flex items-center justify-center h-full w-full bg-[var(--el-fill-color-light)] text-[var(--el-text-color-secondary)]"
+              >
+                <Icon icon="ep:warning" class="mr-2 text-warning" />
+                <span>暂无位置信息</span>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </ContentWrap>
+
+    <!-- 认证信息弹框 -->
+    <Dialog
+      title="设备认证信息"
+      v-model="authDialogVisible"
+      width="640px"
+      :before-close="handleAuthInfoDialogClose"
+    >
+      <el-form :model="authInfo" label-width="120px">
+        <el-form-item label="clientId">
+          <el-input v-model="authInfo.clientId" readonly>
+            <template #append>
+              <el-button @click="copyToClipboard(authInfo.clientId)" type="primary">
+                <Icon icon="ph:copy" />
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="username">
+          <el-input v-model="authInfo.username" readonly>
+            <template #append>
+              <el-button @click="copyToClipboard(authInfo.username)" type="primary">
+                <Icon icon="ph:copy" />
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="password">
+          <el-input
+            v-model="authInfo.password"
+            readonly
+            :type="authPasswordVisible ? 'text' : 'password'"
+          >
+            <template #append>
+              <el-button @click="authPasswordVisible = !authPasswordVisible" type="primary">
+                <Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" />
+              </el-button>
+              <el-button @click="copyToClipboard(authInfo.password)" type="primary">
+                <Icon icon="ph:copy" />
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="handleAuthInfoDialogClose">关闭</el-button>
+      </template>
+    </Dialog>
+  </div>
 
   <!-- TODO 待开发:设备标签 -->
-  <!-- TODO 待开发:设备地图 -->
 </template>
 <script setup lang="ts">
 import { DICT_TYPE } from '@/utils/dict'
 import { ProductVO } from '@/api/iot/product/product'
 import { formatDate } from '@/utils/formatTime'
 import { DeviceVO } from '@/api/iot/device/device'
-import { DeviceApi, MqttConnectionParamsVO } from '@/api/iot/device/device/index'
+import { DeviceApi, IotDeviceAuthInfoVO } from '@/api/iot/device/device'
+import Map from '@/components/Map/index.vue'
+import { ref, computed } from 'vue'
 
 const message = useMessage() // 消息提示
 
 const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
 const emit = defineEmits(['refresh']) // 定义 Emits
 
-const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性
-const passwordVisible = ref(false) // 定义密码可见性状态
-const mqttParams = ref({
-  mqttClientId: '',
-  mqttUsername: '',
-  mqttPassword: ''
-}) // 定义 MQTT 参数对象
+const authDialogVisible = ref(false) // 定义设备认证信息弹框的可见性
+const authPasswordVisible = ref(false) // 定义密码可见性状态
+const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) // 定义设备认证信息对象
+
+// TODO @AI:注释使用 /** */ 风格,方法注释;
+/** 控制地图显示的标志 */
+const showMap = computed(() => {
+  return !!(device.longitude && device.latitude)
+})
+
+/** 获取位置字符串,用于地图组件 */
+const getLocationString = () => {
+  if (device.longitude && device.latitude) {
+    return `${device.longitude},${device.latitude}`
+  }
+  return ''
+}
 
 /** 复制到剪贴板方法 */
 const copyToClipboard = async (text: string) => {
@@ -117,28 +174,20 @@ const copyToClipboard = async (text: string) => {
   }
 }
 
-/** 打开 MQTT 参数弹框的方法 */
-const openMqttParams = async () => {
+/** 打开设备认证信息弹框的方法 */
+const handleAuthInfoDialogOpen = async () => {
   try {
-    const data = await DeviceApi.getMqttConnectionParams(device.id)
-    // 根据 API 响应结构正确获取数据
-    // TODO @haohao:'N/A' 是不是在 ui 里处理哈
-    mqttParams.value = {
-      mqttClientId: data.mqttClientId || 'N/A',
-      mqttUsername: data.mqttUsername || 'N/A',
-      mqttPassword: data.mqttPassword || 'N/A'
-    }
-
-    // 显示 MQTT 弹框
-    mqttDialogVisible.value = true
+    authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id)
+    // 显示设备认证信息弹框
+    authDialogVisible.value = true
   } catch (error) {
-    console.error('获取 MQTT 连接参数出错:', error)
-    message.error('获取MQTT连接参数失败,请检查网络连接或联系管理员')
+    console.error('获取设备认证信息出错:', error)
+    message.error('获取设备认证信息失败,请检查网络连接或联系管理员')
   }
 }
 
-/** 关闭 MQTT 弹框的方法 */
-const handleCloseMqttDialog = () => {
-  mqttDialogVisible.value = false
+/** 关闭设备认证信息弹框的方法 */
+const handleAuthInfoDialogClose = () => {
+  authDialogVisible.value = false
 }
 </script>

+ 0 - 166
src/views/iot/device/device/detail/DeviceDetailsLog.vue

@@ -1,166 +0,0 @@
-<!-- 设备日志 -->
-<template>
-  <ContentWrap>
-    <!-- 搜索区域 -->
-    <el-form :model="queryParams" inline>
-      <el-form-item>
-        <el-select v-model="queryParams.type" placeholder="所有" class="!w-160px">
-          <el-option label="所有" value="" />
-          <!-- TODO @super:搞成枚举 -->
-          <el-option label="状态" value="state" />
-          <el-option label="事件" value="event" />
-          <el-option label="属性" value="property" />
-          <el-option label="服务" value="service" />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-input v-model="queryParams.identifier" placeholder="日志识符" class="!w-200px" />
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" @click="handleQuery">
-          <Icon icon="ep:search" class="mr-5px" /> 搜索
-        </el-button>
-        <el-switch
-          size="large"
-          width="80"
-          v-model="autoRefresh"
-          class="ml-20px"
-          inline-prompt
-          active-text="定时刷新"
-          inactive-text="定时刷新"
-          style="--el-switch-on-color: #13ce66"
-        />
-      </el-form-item>
-    </el-form>
-
-    <!-- 日志列表 -->
-    <el-table v-loading="loading" :data="list" :stripe="true" class="whitespace-nowrap">
-      <el-table-column label="时间" align="center" prop="ts" width="180">
-        <template #default="scope">
-          {{ formatDate(scope.row.ts) }}
-        </template>
-      </el-table-column>
-      <el-table-column label="类型" align="center" prop="type" width="120" />
-      <!-- TODO @super:标识符需要翻译 -->
-      <el-table-column label="标识符" align="center" prop="identifier" width="120" />
-      <el-table-column label="内容" align="center" prop="content" :show-overflow-tooltip="true" />
-    </el-table>
-
-    <!-- 分页 -->
-    <div class="mt-10px flex justify-end">
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getLogList"
-      />
-    </div>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-import { DeviceApi } from '@/api/iot/device/device'
-import { formatDate } from '@/utils/formatTime'
-
-const props = defineProps<{
-  deviceKey: string
-}>()
-
-// 查询参数
-const queryParams = reactive({
-  deviceKey: props.deviceKey,
-  type: '',
-  identifier: '',
-  pageNo: 1,
-  pageSize: 10
-})
-
-// 列表数据
-const loading = ref(false)
-const total = ref(0)
-const list = ref([])
-const autoRefresh = ref(false)
-let timer: any = null // TODO @super:autoRefreshEnable,autoRefreshTimer;对应上
-
-// 类型映射 TODO @super:需要删除么?
-const typeMap = {
-  lifetime: '生命周期',
-  state: '设备状态',
-  property: '属性',
-  event: '事件',
-  service: '服务'
-}
-
-/** 查询日志列表 */
-const getLogList = async () => {
-  if (!props.deviceKey) return
-  loading.value = true
-  try {
-    const data = await DeviceApi.getDeviceLogPage(queryParams)
-    total.value = data.total
-    list.value = data.list
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 获取日志名称 */
-const getLogName = (log: any) => {
-  const { type, identifier } = log
-  let name = '未知'
-
-  if (type === 'property') {
-    if (identifier === 'set_reply') name = '设置回复'
-    else if (identifier === 'report') name = '上报'
-    else if (identifier === 'set') name = '设置'
-  } else if (type === 'state') {
-    name = identifier === 'online' ? '上线' : '下线'
-  } else if (type === 'lifetime') {
-    name = identifier === 'register' ? '注册' : name
-  }
-
-  return `${name}(${identifier})`
-}
-
-/** 搜索操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getLogList()
-}
-
-/** 监听自动刷新 */
-watch(autoRefresh, (newValue) => {
-  if (newValue) {
-    timer = setInterval(() => {
-      getLogList()
-    }, 5000)
-  } else {
-    clearInterval(timer)
-    timer = null
-  }
-})
-
-/** 监听设备标识变化 */
-watch(
-  () => props.deviceKey,
-  (newValue) => {
-    if (newValue) {
-      handleQuery()
-    }
-  }
-)
-
-/** 组件卸载时清除定时器 */
-onBeforeUnmount(() => {
-  if (timer) {
-    clearInterval(timer)
-  }
-})
-
-/** 初始化 */
-onMounted(() => {
-  if (props.deviceKey) {
-    getLogList()
-  }
-})
-</script>

+ 201 - 0
src/views/iot/device/device/detail/DeviceDetailsMessage.vue

@@ -0,0 +1,201 @@
+<!-- 设备消息列表 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索区域 -->
+    <el-form :model="queryParams" inline>
+      <el-form-item>
+        <el-select v-model="queryParams.method" placeholder="所有方法" class="!w-160px" clearable>
+          <el-option
+            v-for="item in methodOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-select
+          v-model="queryParams.upstream"
+          placeholder="上行/下行"
+          class="!w-160px"
+          clearable
+        >
+          <el-option label="上行" value="true" />
+          <el-option label="下行" value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-switch
+          size="large"
+          width="80"
+          v-model="autoRefresh"
+          class="ml-20px"
+          inline-prompt
+          active-text="定时刷新"
+          inactive-text="定时刷新"
+          style="--el-switch-on-color: #13ce66"
+        />
+      </el-form-item>
+    </el-form>
+
+    <!-- 消息列表 -->
+    <el-table v-loading="loading" :data="list" :stripe="true" class="whitespace-nowrap">
+      <el-table-column label="时间" align="center" prop="ts" width="180">
+        <template #default="scope">
+          {{ formatDate(scope.row.ts) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="上行/下行" align="center" prop="upstream" width="140">
+        <template #default="scope">
+          <el-tag :type="scope.row.upstream ? 'primary' : 'success'">
+            {{ scope.row.upstream ? '上行' : '下行' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否回复" align="center" prop="reply" width="140">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.reply" />
+        </template>
+      </el-table-column>
+      <el-table-column label="请求编号" align="center" prop="requestId" width="300" />
+      <el-table-column label="请求方法" align="center" prop="method" width="140">
+        <template #default="scope">
+          {{ methodOptions.find((item) => item.value === scope.row.method)?.label }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="请求/响应数据"
+        align="center"
+        prop="params"
+        :show-overflow-tooltip="true"
+      >
+        <template #default="scope">
+          <span v-if="scope.row.reply">
+            {{ `{"code":${scope.row.code},"msg":"${scope.row.msg}","data":${scope.row.data}\}` }}
+          </span>
+          <span v-else>{{ scope.row.params }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="mt-10px flex justify-end">
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getMessageList"
+      />
+    </div>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { DeviceApi } from '@/api/iot/device/device'
+import { formatDate } from '@/utils/formatTime'
+import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+  deviceId: number
+}>()
+
+// 查询参数
+const queryParams = reactive({
+  deviceId: props.deviceId,
+  method: undefined,
+  upstream: undefined,
+  pageNo: 1,
+  pageSize: 10
+})
+
+// 列表数据
+const loading = ref(false)
+const total = ref(0)
+const list = ref([])
+const autoRefresh = ref(false) // 自动刷新开关
+let autoRefreshTimer: any = null // 自动刷新定时器
+
+// 消息方法选项
+const methodOptions = computed(() => {
+  return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
+    label: item.name,
+    value: item.method
+  }))
+})
+
+/** 查询消息列表 */
+const getMessageList = async () => {
+  if (!props.deviceId) return
+  loading.value = true
+  try {
+    const data = await DeviceApi.getDeviceMessagePage(queryParams)
+    total.value = data.total
+    list.value = data.list
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getMessageList()
+}
+
+/** 监听自动刷新 */
+watch(autoRefresh, (newValue) => {
+  if (newValue) {
+    autoRefreshTimer = setInterval(() => {
+      getMessageList()
+    }, 5000)
+  } else {
+    clearInterval(autoRefreshTimer)
+    autoRefreshTimer = null
+  }
+})
+
+/** 监听设备标识变化 */
+watch(
+  () => props.deviceId,
+  (newValue) => {
+    if (newValue) {
+      handleQuery()
+    }
+  }
+)
+
+/** 组件卸载时清除定时器 */
+onBeforeUnmount(() => {
+  if (autoRefreshTimer) {
+    clearInterval(autoRefreshTimer)
+    autoRefreshTimer = null
+  }
+})
+
+/** 初始化 */
+onMounted(() => {
+  if (props.deviceId) {
+    getMessageList()
+  }
+})
+
+/** 刷新消息列表 */
+const refresh = (delay = 0) => {
+  if (delay > 0) {
+    setTimeout(() => {
+      handleQuery()
+    }, delay)
+  } else {
+    handleQuery()
+  }
+}
+
+/** 暴露方法给父组件 */
+defineExpose({
+  refresh
+})
+</script>

+ 0 - 134
src/views/iot/device/device/detail/DeviceDetailsModel.vue

@@ -1,134 +0,0 @@
-<!-- 设备物模型:运行状态(属性)、事件管理、服务调用 -->
-<template>
-  <ContentWrap>
-    <el-tabs v-model="activeTab">
-      <el-tab-pane label="运行状态" name="status">
-        <ContentWrap>
-          <!-- 搜索工作栏 -->
-          <el-form
-            class="-mb-15px"
-            :model="queryParams"
-            ref="queryFormRef"
-            :inline="true"
-            label-width="68px"
-          >
-            <el-form-item label="标识符" prop="identifier">
-              <el-input
-                v-model="queryParams.identifier"
-                placeholder="请输入标识符"
-                clearable
-                class="!w-240px"
-              />
-            </el-form-item>
-            <el-form-item label="属性名称" prop="name">
-              <el-input
-                v-model="queryParams.name"
-                placeholder="请输入属性名称"
-                clearable
-                class="!w-240px"
-              />
-            </el-form-item>
-            <el-form-item>
-              <el-button @click="handleQuery"
-                ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
-              >
-              <el-button @click="resetQuery"
-                ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
-              >
-            </el-form-item>
-          </el-form>
-        </ContentWrap>
-        <ContentWrap>
-          <el-tabs>
-            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-              <el-table-column label="属性标识符" align="center" prop="property.identifier" />
-              <el-table-column label="属性名称" align="center" prop="property.name" />
-              <el-table-column label="数据类型" align="center" prop="property.dataType" />
-              <el-table-column label="属性值" align="center" prop="value" />
-              <el-table-column
-                label="更新时间"
-                align="center"
-                prop="updateTime"
-                :formatter="dateFormatter"
-                width="180px"
-              />
-              <el-table-column label="操作" align="center">
-                <template #default="scope">
-                  <el-button
-                    link
-                    type="primary"
-                    @click="openDetail(props.device.id, scope.row.property.identifier)"
-                  >
-                    查看数据
-                  </el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </el-tabs>
-          <!-- 表单弹窗:添加/修改 -->
-          <DeviceDataDetail ref="detailRef" :device="device" :product="product" />
-        </ContentWrap>
-      </el-tab-pane>
-      <el-tab-pane label="事件管理" name="event">
-        <p>事件管理</p>
-      </el-tab-pane>
-      <el-tab-pane label="服务调用" name="service">
-        <p>服务调用</p>
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-</template>
-<script setup lang="ts">
-import { ProductVO } from '@/api/iot/product/product'
-import { DeviceApi, DeviceDataVO, DeviceVO } from '@/api/iot/device/device'
-import { dateFormatter } from '@/utils/formatTime'
-import DeviceDataDetail from './DeviceDataDetail.vue'
-
-const props = defineProps<{ product: ProductVO; device: DeviceVO }>()
-
-const loading = ref(true) // 列表的加载中
-const list = ref<DeviceDataVO[]>([]) // 列表的数据
-const queryParams = reactive({
-  deviceId: -1,
-  identifier: undefined as string | undefined,
-  name: undefined as string | undefined
-})
-
-const queryFormRef = ref() // 搜索的表单
-const activeTab = ref('status') // 默认选中的标签
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    queryParams.deviceId = props.device.id
-    list.value = await DeviceApi.getLatestDeviceProperties(queryParams)
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  queryParams.identifier = undefined
-  queryParams.name = undefined
-  handleQuery()
-}
-
-/** 添加/修改操作 */
-const detailRef = ref()
-const openDetail = (deviceId: number, identifier: string) => {
-  detailRef.value.open(deviceId, identifier)
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>

+ 298 - 209
src/views/iot/device/device/detail/DeviceDetailsSimulator.vue

@@ -6,75 +6,109 @@
       <el-col :span="12">
         <el-tabs v-model="activeTab" type="border-card">
           <!-- 上行指令调试 -->
-          <el-tab-pane label="上行指令调试" name="up">
-            <el-tabs v-if="activeTab === 'up'" v-model="subTab">
+          <el-tab-pane label="上行指令调试" name="upstream">
+            <el-tabs v-if="activeTab === 'upstream'" v-model="upstreamTab">
               <!-- 属性上报 -->
-              <el-tab-pane label="属性上报" name="property">
+              <el-tab-pane label="属性上报" :name="IotDeviceMessageMethodEnum.PROPERTY_POST.method">
                 <ContentWrap>
-                  <el-table
-                    v-loading="loading"
-                    :data="list"
-                    :show-overflow-tooltip="true"
-                    :stripe="true"
-                  >
-                    <!-- TODO @super:每个 colum 搞下宽度,避免 table 每一列最后有个 . -->
-                    <!-- TODO @super:可以左侧 fixed -->
-                    <el-table-column align="center" label="功能名称" prop="name" />
-                    <el-table-column align="center" label="标识符" prop="identifier" />
-                    <el-table-column align="center" label="数据类型" prop="identifier">
-                      <!-- TODO @super:不用翻译,可以减少宽度的占用 -->
+                  <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="功能名称"
+                      prop="name"
+                      width="120"
+                    />
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="center" label="数据类型" width="100">
                       <template #default="{ row }">
-                        {{ dataTypeOptionsLabel(row.property?.dataType) ?? '-' }}
+                        {{ row.property?.dataType ?? '-' }}
                       </template>
                     </el-table-column>
-                    <el-table-column align="left" label="数据定义" prop="identifier">
+                    <el-table-column align="left" label="数据定义" min-width="200">
                       <template #default="{ row }">
                         <DataDefinition :data="row" />
                       </template>
                     </el-table-column>
-                    <!-- TODO @super:可以右侧 fixed -->
-                    <el-table-column align="center" label="值" width="80">
+                    <el-table-column fixed="right" align="center" label="值" width="150">
                       <template #default="scope">
-                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          placeholder="输入值"
+                          size="small"
+                        />
                       </template>
                     </el-table-column>
                   </el-table>
-                  <!-- TODO @super:发送按钮,可以放在右侧哈。因为我们的 simulateValue 就在最右侧 -->
-                  <div class="mt-10px">
-                    <el-button type="primary" @click="handlePropertyReport"> 发送</el-button>
+                  <div class="flex justify-between items-center mt-4">
+                    <span class="text-sm text-gray-600">
+                      设置属性值后,点击「发送属性上报」按钮
+                    </span>
+                    <el-button type="primary" @click="handlePropertyPost">发送属性上报</el-button>
                   </div>
                 </ContentWrap>
               </el-tab-pane>
 
               <!-- 事件上报 -->
-              <!-- TODO @super:待实现 -->
-              <el-tab-pane label="事件上报" name="event">
+              <el-tab-pane label="事件上报" :name="IotDeviceMessageMethodEnum.EVENT_POST.method">
                 <ContentWrap>
-                  <!-- TODO @super:因为事件是每个 event 去模拟,而不是类似属性的批量上传。所以,可以每一列后面有个“模拟”按钮。另外,“值”使用 textarea,高度 3 -->
-                  <!-- <el-table v-loading="loading" :data="eventList" :stripe="true">
-                    <el-table-column label="功能名称" align="center" prop="name" />
-                    <el-table-column label="标识符" align="center" prop="identifier" />
-                    <el-table-column label="数据类型" align="center" prop="dataType" />
+                  <el-table :data="eventList" :show-overflow-tooltip="true" :stripe="true">
                     <el-table-column
-                      label="数据定义"
+                      fixed="left"
                       align="center"
-                      prop="specs"
-                      :show-overflow-tooltip="true"
+                      label="功能名称"
+                      prop="name"
+                      width="120"
                     />
-                    <el-table-column label="值" align="center" width="80">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="center" label="数据类型" width="100">
+                      <template #default="{ row }">
+                        {{ row.event?.dataType ?? '-' }}
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="left" label="数据定义" min-width="200">
+                      <template #default="{ row }">
+                        <DataDefinition :data="row" />
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="center" label="值" width="200">
                       <template #default="scope">
-                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          type="textarea"
+                          :rows="3"
+                          placeholder="输入事件参数(JSON格式)"
+                          size="small"
+                        />
+                      </template>
+                    </el-table-column>
+                    <el-table-column fixed="right" align="center" label="操作" width="100">
+                      <template #default="scope">
+                        <el-button type="primary" size="small" @click="handleEventPost(scope.row)">
+                          上报事件
+                        </el-button>
                       </template>
                     </el-table-column>
                   </el-table>
-                  <div class="mt-10px">
-                    <el-button type="primary" @click="handleEventReport">发送</el-button>
-                  </div> -->
                 </ContentWrap>
               </el-tab-pane>
 
               <!-- 状态变更 -->
-              <el-tab-pane label="状态变更" name="status">
+              <el-tab-pane label="状态变更" :name="IotDeviceMessageMethodEnum.STATE_UPDATE.method">
                 <ContentWrap>
                   <div class="flex gap-4">
                     <el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)">
@@ -90,39 +124,106 @@
           </el-tab-pane>
 
           <!-- 下行指令调试 -->
-          <!-- TODO @super:待实现 -->
-          <el-tab-pane label="下行指令调试" name="down">
-            <el-tabs v-if="activeTab === 'down'" v-model="subTab">
+          <el-tab-pane label="下行指令调试" name="downstream">
+            <el-tabs v-if="activeTab === 'downstream'" v-model="downstreamTab">
               <!-- 属性调试 -->
-              <el-tab-pane label="属性调试" name="propertyDebug">
+              <el-tab-pane label="属性设置" :name="IotDeviceMessageMethodEnum.PROPERTY_SET.method">
                 <ContentWrap>
-                  <!-- <el-table v-loading="loading" :data="propertyList" :stripe="true">
-                    <el-table-column label="功能名称" align="center" prop="name" />
-                    <el-table-column label="标识符" align="center" prop="identifier" />
-                    <el-table-column label="数据类型" align="center" prop="dataType" />
+                  <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
                     <el-table-column
-                      label="数据定义"
+                      fixed="left"
                       align="center"
-                      prop="specs"
-                      :show-overflow-tooltip="true"
+                      label="功能名称"
+                      prop="name"
+                      width="120"
                     />
-                    <el-table-column label="值" align="center" width="80">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="center" label="数据类型" width="100">
+                      <template #default="{ row }">
+                        {{ row.property?.dataType ?? '-' }}
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="left" label="数据定义" min-width="200">
+                      <template #default="{ row }">
+                        <DataDefinition :data="row" />
+                      </template>
+                    </el-table-column>
+                    <el-table-column fixed="right" align="center" label="值" width="150">
                       <template #default="scope">
-                        <el-input v-model="scope.row.simulateValue" class="!w-60px" />
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          placeholder="输入值"
+                          size="small"
+                        />
                       </template>
                     </el-table-column>
                   </el-table>
-                  <div class="mt-10px">
-                    <el-button type="primary" @click="handlePropertyGet">获取</el-button>
-                  </div> -->
+                  <div class="flex justify-between items-center mt-4">
+                    <span class="text-sm text-gray-600">
+                      设置属性值后,点击「发送属性设置」按钮
+                    </span>
+                    <el-button type="primary" @click="handlePropertySet">发送属性设置</el-button>
+                  </div>
                 </ContentWrap>
               </el-tab-pane>
 
               <!-- 服务调用 -->
-              <!-- TODO @super:待实现 -->
-              <el-tab-pane label="服务调用" name="service">
+              <el-tab-pane
+                label="设备服务调用"
+                :name="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
+              >
                 <ContentWrap>
-                  <!-- 服务调用相关内容 -->
+                  <el-table :data="serviceList" :show-overflow-tooltip="true" :stripe="true">
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="服务名称"
+                      prop="name"
+                      width="120"
+                    />
+                    <el-table-column
+                      fixed="left"
+                      align="center"
+                      label="标识符"
+                      prop="identifier"
+                      width="120"
+                    />
+                    <el-table-column align="left" label="输入参数" min-width="200">
+                      <template #default="{ row }">
+                        <DataDefinition :data="row" />
+                      </template>
+                    </el-table-column>
+                    <el-table-column align="center" label="参数值" width="200">
+                      <template #default="scope">
+                        <el-input
+                          :model-value="getFormValue(scope.row.identifier)"
+                          @update:model-value="setFormValue(scope.row.identifier, $event)"
+                          type="textarea"
+                          :rows="3"
+                          placeholder="输入服务参数(JSON格式)"
+                          size="small"
+                        />
+                      </template>
+                    </el-table-column>
+                    <el-table-column fixed="right" align="center" label="操作" width="100">
+                      <template #default="scope">
+                        <el-button
+                          type="primary"
+                          size="small"
+                          @click="handleServiceInvoke(scope.row)"
+                        >
+                          服务调用
+                        </el-button>
+                      </template>
+                    </el-table-column>
+                  </el-table>
                 </ContentWrap>
               </el-tab-pane>
             </el-tabs>
@@ -132,11 +233,9 @@
 
       <!-- 右侧设备日志区域 -->
       <el-col :span="12">
-        <el-tabs type="border-card">
-          <el-tab-pane label="设备日志">
-            <DeviceDetailsLog :device-key="device.deviceKey" />
-          </el-tab-pane>
-        </el-tabs>
+        <ContentWrap title="设备消息">
+          <DeviceDetailsMessage ref="deviceMessageRef" :device-id="device.id" />
+        </ContentWrap>
       </el-col>
     </el-row>
   </ContentWrap>
@@ -144,188 +243,178 @@
 
 <script lang="ts" setup>
 import { ProductVO } from '@/api/iot/product/product'
-import { SimulatorData, ThingModelApi } from '@/api/iot/thingmodel'
+import { ThingModelData } from '@/api/iot/thingmodel'
 import { DeviceApi, DeviceStateEnum, DeviceVO } from '@/api/iot/device/device'
-import DeviceDetailsLog from './DeviceDetailsLog.vue'
-import { getDataTypeOptionsLabel } from '@/views/iot/thingmodel/config'
+import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
 import { DataDefinition } from '@/views/iot/thingmodel/components'
+import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
 
 const props = defineProps<{
   product: ProductVO
   device: DeviceVO
+  thingModelList: ThingModelData[]
 }>()
 
 const message = useMessage() // 消息弹窗
-const activeTab = ref('up') // TODO @super:upstream 上行、downstream 下行
-const subTab = ref('property') // TODO @super:upstreamTab
+const activeTab = ref('upstream') // 上行upstream、下行downstream
+const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method) // 上行子标签
+const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method) // 下行子标签
+const deviceMessageRef = ref() // 设备消息组件引用
+const deviceMessageRefreshDelay = 2000 // 延迟 N 秒,保证模拟上行的消息被处理
 
-const loading = ref(false)
-const queryParams = reactive({
-  type: undefined, // TODO @super:type 默认给个第一个 tab 对应的,避免下面 watch 爆红
-  productId: -1
-})
-const list = ref<SimulatorData[]>([]) // 物模型列表的数据 TODO @super:thingModelList
-// TODO @super:dataTypeOptionsLabel 是不是不用定义,直接用 getDataTypeOptionsLabel 在 template 中使用即可?
-const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型
+// 表单数据:存储用户输入的模拟值
+const formData = ref<Record<string, string>>({})
 
-/** 查询物模型列表 */
-// TODO @super:getThingModelList 更精准
-const getList = async () => {
-  loading.value = true
-  try {
-    queryParams.productId = props.product?.id || -1
-    const data = await ThingModelApi.getThingModelList(queryParams)
-    // 转换数据,添加 simulateValue 字段
-    // TODO @super:貌似下面的 simulateValue 不设置也可以?
-    list.value = data.map((item) => ({
-      ...item,
-      simulateValue: ''
-    }))
-  } finally {
-    loading.value = false
-  }
+// 根据类型过滤物模型数据
+const getFilteredThingModelList = (type: number) => {
+  return props.thingModelList.filter((item) => item.type === type)
 }
+const propertyList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY))
+const eventList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.EVENT))
+const serviceList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE))
 
-// // 功能列表数据结构定义
-// interface TableItem {
-//   name: string
-//   identifier: string
-//   value: string | number
-// }
-
-// // 添加计算属性来过滤物模型数据
-// const propertyList = computed(() => {
-//   return list.value
-//     .filter((item) => item.type === 'property')
-//     .map((item) => ({
-//       name: item.name,
-//       identifier: item.identifier,
-//       value: ''
-//     }))
-// })
-
-// const eventList = computed(() => {
-//   return list.value
-//     .filter((item) => item.type === 'event')
-//     .map((item) => ({
-//       name: item.name,
-//       identifier: item.identifier,
-//       value: ''
-//     }))
-// })
-
-/** 监听标签页变化 */
-// todo:后续改成查询字典
-watch(
-  [activeTab, subTab],
-  ([newActiveTab, newSubTab]) => {
-    // 根据标签页设置查询类型
-    if (newActiveTab === 'up') {
-      switch (newSubTab) {
-        case 'property':
-          queryParams.type = 1
-          break
-        case 'event':
-          queryParams.type = 3
-          break
-        // case 'status':
-        //   queryParams.type = 'status'
-        //   break
-      }
-    } else if (newActiveTab === 'down') {
-      switch (newSubTab) {
-        case 'propertyDebug':
-          queryParams.type = 1
-          break
-        case 'service':
-          queryParams.type = 2
-          break
-      }
-    }
-    getList() // 切换标签时重新获取数据
-  },
-  { immediate: true }
-)
+/** 获取表单值的辅助函数 */
+const getFormValue = (identifier: string | number | undefined) => {
+  if (!identifier) return ''
+  return formData.value[String(identifier)] || ''
+}
+/** 设置表单值的辅助函数 */
+const setFormValue = (identifier: string | number | undefined, value: string) => {
+  if (!identifier) return
+  formData.value[String(identifier)] = value
+}
 
-/** 处理属性上报 */
-const handlePropertyReport = async () => {
-  // TODO @super:数据类型效验
-  const data: Record<string, object> = {}
-  list.value.forEach((item) => {
-    // 只有当 simulateValue 有值时才添加到 content 中
-    // TODO @super:直接 if (item.simulateValue) 就可以哈,js 这块还是比较灵活的
-    if (item.simulateValue !== undefined && item.simulateValue !== '') {
-      // TODO @super:这里有个红色的 idea 告警,觉得去除下
-      data[item.identifier] = item.simulateValue
+/** 模拟属性上报 */
+const handlePropertyPost = async () => {
+  const data: Record<string, any> = {}
+  propertyList.value.forEach((item) => {
+    const value = getFormValue(item.identifier)
+    if (value && item.identifier) {
+      data[String(item.identifier)] = value
     }
   })
+  if (Object.keys(data).length === 0) {
+    message.warning('请至少设置一个属性值')
+    return
+  }
 
   try {
-    await DeviceApi.upstreamDevice({
-      id: props.device.id,
-      type: 'property',
-      identifier: 'report',
-      data: data
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
+      params: data
     })
     message.success('属性上报成功')
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
   } catch (error) {
     message.error('属性上报失败')
   }
 }
 
-// // 处理事件上报
-// const handleEventReport = async () => {
-//   const contentObj: Record<string, any> = {}
-//   list.value
-//     .filter(item => item.type === 'event')
-//     .forEach((item) => {
-//       if (item.simulateValue !== undefined && item.simulateValue !== '') {
-//         contentObj[item.identifier] = item.simulateValue
-//       }
-//     })
-
-//   const reportData: ReportData = {
-//     productKey: props.product.productKey,
-//     deviceKey: props.device.deviceKey,
-//     type: 'event',
-//     subType: list.value.find(item => item.type === 'event')?.identifier || '',
-//     reportTime: new Date().toISOString(),
-//     content: JSON.stringify(contentObj)  // 转换为 JSON 字符串
-//   }
+/** 模拟事件上报 */
+const handleEventPost = async (eventItem: ThingModelData) => {
+  const value = getFormValue(eventItem.identifier)
+  if (!value) {
+    message.warning('请输入事件参数')
+    return
+  }
+  let eventParams: any
+  try {
+    eventParams = JSON.parse(value)
+  } catch {
+    message.error('事件参数格式不正确,请输入有效的JSON格式')
+    return
+  }
 
-//   try {
-//     // TODO: 调用API发送数据
-//     console.log('上报数据:', reportData)
-//     message.success('事件上报成功')
-//   } catch (error) {
-//     message.error('事件上报失败')
-//   }
-// }
+  try {
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.EVENT_POST.method,
+      params: {
+        identifier: String(eventItem.identifier),
+        value: eventParams,
+        time: Date.now()
+      }
+    })
+    message.success(`事件【${String(eventItem.name)}】上报成功`)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+  } catch (error) {
+    message.error(`事件【${String(eventItem.name)}】上报失败`)
+  }
+}
 
-/** 处理设备状态 */
+/** 模拟设备状态 */
 const handleDeviceState = async (state: number) => {
   try {
-    await DeviceApi.upstreamDevice({
-      id: props.device.id,
-      type: 'state',
-      identifier: 'report',
-      data: state
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
+      params: {
+        state: state
+      }
     })
     message.success(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}成功`)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
   } catch (error) {
     message.error(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}失败`)
   }
 }
 
-// 处理属性获取
-const handlePropertyGet = async () => {
-  // TODO: 实现属性获取逻辑
-  message.success('属性获取成功')
+/** 模拟属性设置 */
+const handlePropertySet = async () => {
+  const data: Record<string, any> = {}
+  propertyList.value.forEach((item) => {
+    const value = getFormValue(item.identifier)
+    if (value && item.identifier) {
+      data[String(item.identifier)] = value
+    }
+  })
+  if (Object.keys(data).length === 0) {
+    message.warning('请至少设置一个属性值')
+    return
+  }
+
+  try {
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
+      params: data
+    })
+    message.success('属性设置成功')
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+  } catch (error) {
+    message.error('属性设置失败')
+  }
 }
 
-// 初始化
-onMounted(() => {
-  getList()
-})
-// TODO @芋艿:后续再详细 review 下;
+/** 模拟服务调用 */
+const handleServiceInvoke = async (serviceItem: ThingModelData) => {
+  const value = getFormValue(serviceItem.identifier)
+  if (!value) {
+    message.warning('请输入服务参数')
+    return
+  }
+  let serviceParams: any
+  try {
+    serviceParams = JSON.parse(value)
+  } catch {
+    message.error('服务参数格式不正确,请输入有效的JSON格式')
+    return
+  }
+
+  try {
+    await DeviceApi.sendDeviceMessage({
+      deviceId: props.device.id,
+      method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
+      params: {
+        identifier: String(serviceItem.identifier),
+        inputParams: serviceParams
+      }
+    })
+    message.success(`服务【${String(serviceItem.name)}】调用成功`)
+    deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+  } catch (error) {
+    message.error(`服务【${String(serviceItem.name)}】调用失败`)
+  }
+}
 </script>

+ 35 - 0
src/views/iot/device/device/detail/DeviceDetailsThingModel.vue

@@ -0,0 +1,35 @@
+<!-- 设备物模型:设备属性、事件管理、服务调用 -->
+<template>
+  <ContentWrap>
+    <el-tabs v-model="activeTab">
+      <el-tab-pane label="设备属性(运行状态)" name="property">
+        <DeviceDetailsThingModelProperty :device-id="deviceId" />
+      </el-tab-pane>
+      <el-tab-pane label="设备事件上报" name="event">
+        <DeviceDetailsThingModelEvent
+          :device-id="props.deviceId"
+          :thing-model-list="props.thingModelList"
+        />
+      </el-tab-pane>
+      <el-tab-pane label="设备服务调用" name="service">
+        <DeviceDetailsThingModelService
+          :device-id="deviceId"
+          :thing-model-list="props.thingModelList"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { ThingModelData } from '@/api/iot/thingmodel'
+import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue'
+import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue'
+import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue'
+
+const props = defineProps<{
+  deviceId: number
+  thingModelList: ThingModelData[]
+}>()
+
+const activeTab = ref('property') // 默认选中设备属性
+</script>

+ 192 - 0
src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue

@@ -0,0 +1,192 @@
+<!-- 设备事件管理 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+      @submit.prevent
+    >
+      <el-form-item label="标识符" prop="identifier">
+        <el-select
+          v-model="queryParams.identifier"
+          placeholder="请选择事件标识符"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="event in eventThingModels"
+            :key="event.identifier"
+            :label="`${event.name}(${event.identifier})`"
+            :value="event.identifier!"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围" prop="times">
+        <el-date-picker
+          v-model="queryParams.times"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+          class="!w-360px"
+          :shortcuts="defaultShortcuts"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <!-- 事件列表 -->
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="上报时间" align="center" prop="reportTime" width="180px">
+        <template #default="scope">
+          {{ scope.row.request?.reportTime ? formatDate(scope.row.request.reportTime) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="标识符" align="center" prop="identifier" width="160px">
+        <template #default="scope">
+          <el-tag type="primary" size="small">
+            {{ scope.row.request?.identifier }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="事件名称" align="center" prop="eventName" width="160px">
+        <template #default="scope">
+          {{ getEventName(scope.row.request?.identifier) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="事件类型" align="center" prop="eventType" width="100px">
+        <template #default="scope">
+          {{ getEventType(scope.row.request?.identifier) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="输入参数" align="center" prop="params">
+        <template #default="scope"> {{ parseParams(scope.row.request.params) }} </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelData } from '@/api/iot/thingmodel'
+import { formatDate, defaultShortcuts } from '@/utils/formatTime'
+import {
+  getEventTypeLabel,
+  IotDeviceMessageMethodEnum,
+  IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+  deviceId: number
+  thingModelList: ThingModelData[]
+}>()
+
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([] as any[]) // 列表的数据
+const queryParams = reactive({
+  deviceId: props.deviceId,
+  method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息
+  identifier: '',
+  times: [] as any[],
+  pageNo: 1,
+  pageSize: 10
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 事件类型的物模型数据 */
+const eventThingModels = computed(() => {
+  return props.thingModelList.filter(
+    (item: ThingModelData) => item.type === IoTThingModelTypeEnum.EVENT
+  )
+})
+
+/** 查询列表 */
+const getList = async () => {
+  if (!props.deviceId) return
+  loading.value = true
+  try {
+    const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
+    list.value = data.list
+    total.value = data.length
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.identifier = ''
+  queryParams.times = []
+  handleQuery()
+}
+
+/** 获取事件名称 */
+const getEventName = (identifier: string | undefined) => {
+  if (!identifier) return '-'
+  const event = eventThingModels.value.find(
+    (item: ThingModelData) => item.identifier === identifier
+  )
+  return event?.name || identifier
+}
+
+/** 获取事件类型 */
+const getEventType = (identifier: string | undefined) => {
+  if (!identifier) return '-'
+  const event = eventThingModels.value.find(
+    (item: ThingModelData) => item.identifier === identifier
+  )
+  if (!event?.event?.type) return '-'
+  return getEventTypeLabel(event.event.type) || '-'
+}
+
+/** 解析参数 */
+const parseParams = (params: string) => {
+  try {
+    const parsed = JSON.parse(params)
+    if (parsed.params) {
+      return parsed.params
+    }
+    return parsed
+  } catch (error) {
+    return {}
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 245 - 0
src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue

@@ -0,0 +1,245 @@
+<!-- 设备属性管理 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+      @submit.prevent
+    >
+      <el-form-item label="" prop="keyword">
+        <el-input
+          v-model="queryParams.keyword"
+          placeholder="请输入属性名称、标志符"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+          @clear="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item class="float-right !mr-0 !mb-0">
+        <el-button-group>
+          <el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
+            <Icon icon="ep:grid" />
+          </el-button>
+          <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
+            <Icon icon="ep:list" />
+          </el-button>
+        </el-button-group>
+      </el-form-item>
+      <!-- TODO @芋艿:参考阿里云,实时刷新! -->
+      <el-form-item>
+        <el-switch
+          size="large"
+          width="80"
+          v-model="autoRefresh"
+          class="-ml-15px"
+          inline-prompt
+          active-text="定时刷新"
+          inactive-text="定时刷新"
+          style="--el-switch-on-color: #13ce66"
+        />
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+  <ContentWrap>
+    <!-- 卡片视图 -->
+    <template v-if="viewMode === 'card'">
+      <el-row :gutter="16" v-loading="loading">
+        <el-col
+          v-for="item in list"
+          :key="item.identifier"
+          :xs="24"
+          :sm="12"
+          :md="12"
+          :lg="6"
+          class="mb-4"
+        >
+          <el-card
+            class="h-full transition-colors relative overflow-hidden"
+            :body-style="{ padding: '0' }"
+          >
+            <!-- 添加渐变背景层 -->
+            <div
+              class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none bg-gradient-to-b from-[#eefaff] to-transparent"
+            >
+            </div>
+            <div class="p-4 relative">
+              <!-- 标题区域 -->
+              <div class="flex items-center mb-3">
+                <div class="mr-2.5 flex items-center">
+                  <Icon icon="ep:cpu" class="text-[18px] text-[#0070ff]" />
+                </div>
+                <div class="text-[16px] font-600 flex-1">{{ item.name }}</div>
+                <!-- 标识符 -->
+                <div class="inline-flex items-center mr-2">
+                  <el-tag size="small" type="primary">
+                    {{ item.identifier }}
+                  </el-tag>
+                </div>
+                <!-- 数据类型标签 -->
+                <div class="inline-flex items-center mr-2">
+                  <el-tag size="small" type="info">
+                    {{ item.dataType }}
+                  </el-tag>
+                </div>
+                <!-- 数据图标 - 可点击 -->
+                <div
+                  class="cursor-pointer flex items-center justify-center w-8 h-8 rounded-full hover:bg-blue-50 transition-colors"
+                  @click="openHistory(props.deviceId, item.identifier, item.dataType)"
+                >
+                  <Icon icon="ep:data-line" class="text-[18px] text-[#0070ff]" />
+                </div>
+              </div>
+
+              <!-- 信息区域 -->
+              <div class="text-[14px]">
+                <div class="mb-2.5 last:mb-0">
+                  <span class="text-[#717c8e] mr-2.5">属性值</span>
+                  <span class="text-[#0b1d30] font-600">
+                    {{ formatValueWithUnit(item) }}
+                  </span>
+                </div>
+                <div class="mb-2.5 last:mb-0">
+                  <span class="text-[#717c8e] mr-2.5">更新时间</span>
+                  <span class="text-[#0b1d30] text-[12px]">
+                    {{ item.updateTime ? formatDate(item.updateTime) : '-' }}
+                  </span>
+                </div>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </template>
+
+    <!-- 列表视图 -->
+    <el-table v-else v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="属性标识符" align="center" prop="identifier" />
+      <el-table-column label="属性名称" align="center" prop="name" />
+      <el-table-column label="数据类型" align="center" prop="dataType" />
+      <el-table-column label="属性值" align="center" prop="value">
+        <template #default="scope">
+          {{ formatValueWithUnit(scope.row) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="更新时间"
+        align="center"
+        prop="updateTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openHistory(props.deviceId, scope.row.identifier, scope.row.dataType)"
+          >
+            查看数据
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 表单弹窗:添加/修改 -->
+    <DeviceDetailsThingModelPropertyHistory ref="historyRef" :deviceId="props.deviceId" />
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DeviceApi, IotDevicePropertyDetailRespVO } from '@/api/iot/device/device'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'
+
+const props = defineProps<{ deviceId: number }>()
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IotDevicePropertyDetailRespVO[]>([]) // 显示的列表数据
+const filterList = ref<IotDevicePropertyDetailRespVO[]>([]) // 完整的数据列表
+const queryParams = reactive({
+  keyword: '' as string
+})
+const autoRefresh = ref(false) // 自动刷新开关
+let autoRefreshTimer: any = null // 定时器
+const viewMode = ref<'card' | 'list'>('card') // 视图模式状态
+
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const params = {
+      deviceId: props.deviceId,
+      identifier: undefined as string | undefined,
+      name: undefined as string | undefined
+    }
+    filterList.value = await DeviceApi.getLatestDeviceProperties(params)
+    handleFilter()
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 前端筛选数据 */
+const handleFilter = () => {
+  if (!queryParams.keyword.trim()) {
+    list.value = filterList.value
+  } else {
+    const keyword = queryParams.keyword.toLowerCase()
+    list.value = filterList.value.filter(
+      (item) =>
+        item.identifier?.toLowerCase().includes(keyword) ||
+        item.name?.toLowerCase().includes(keyword)
+    )
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  handleFilter()
+}
+
+/** 历史操作 */
+const historyRef = ref()
+const openHistory = (deviceId: number, identifier: string, dataType: string) => {
+  historyRef.value.open(deviceId, identifier, dataType)
+}
+
+/** 格式化属性值和单位 */
+const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => {
+  if (item.value === null || item.value === undefined || item.value === '') {
+    return '-'
+  }
+  const unitName = item.dataSpecs?.unitName
+  return unitName ? `${item.value} ${unitName}` : item.value
+}
+
+/** 监听自动刷新 */
+watch(autoRefresh, (newValue) => {
+  if (newValue) {
+    autoRefreshTimer = setInterval(() => {
+      getList()
+    }, 5000) // 每 5 秒刷新一次
+  } else {
+    clearInterval(autoRefreshTimer)
+    autoRefreshTimer = null
+  }
+})
+
+/** 组件卸载时清除定时器 */
+onBeforeUnmount(() => {
+  if (autoRefreshTimer) {
+    clearInterval(autoRefreshTimer)
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 216 - 0
src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue

@@ -0,0 +1,216 @@
+<!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)-->
+<template>
+  <Dialog title="查看数据" v-model="dialogVisible" width="1024px" :appendToBody="true">
+    <ContentWrap>
+      <!-- 搜索工作栏 -->
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item label="" prop="createTime">
+          <el-date-picker
+            v-model="queryParams.times"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="datetimerange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            class="!w-360px"
+            @change="handleTimeChange"
+            :shortcuts="defaultShortcuts"
+          />
+        </el-form-item>
+        <el-form-item class="float-right !mr-0 !mb-0">
+          <el-button-group>
+            <el-button
+              :type="viewMode === 'chart' ? 'primary' : 'default'"
+              @click="viewMode = 'chart'"
+              :disabled="isComplexDataType"
+            >
+              <Icon icon="ep:histogram" />
+            </el-button>
+            <el-button
+              :type="viewMode === 'list' ? 'primary' : 'default'"
+              @click="viewMode = 'list'"
+            >
+              <Icon icon="ep:list" />
+            </el-button>
+          </el-button-group>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 数据展示区域 -->
+    <ContentWrap>
+      <!-- 图表模式 -->
+      <div v-if="viewMode === 'chart'" class="chart-container">
+        <div v-if="list.length === 0" class="text-center text-gray-500 py-20"> 暂无数据 </div>
+        <Echart v-else :key="'erchart' + Date.now()" :options="echartsOption" height="400px" />
+      </div>
+
+      <!-- 表格模式 -->
+      <div v-else>
+        <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+          <el-table-column label="时间" align="center" prop="time" width="180px">
+            <template #default="scope">
+              {{ formatDate(new Date(scope.row.updateTime)) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="属性值" align="center" prop="value">
+            <template #default="scope">
+              {{ scope.row.value }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </ContentWrap>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DeviceApi, IotDevicePropertyRespVO } from '@/api/iot/device/device'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { Echart } from '@/components/Echart'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+
+defineProps<{ deviceId: number }>()
+
+/** IoT 设备属性历史数据详情 */
+defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' })
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(false)
+const viewMode = ref<'chart' | 'list'>('chart') // 视图模式状态
+const list = ref<IotDevicePropertyRespVO[]>([]) // 列表的数据
+const chartKey = ref(0) // 图表重新渲染的key
+const thingModelDataType = ref<string>('') // 物模型数据类型
+const queryParams = reactive({
+  deviceId: -1,
+  identifier: '',
+  times: [
+    // 默认显示最近一周的数据
+    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+    formatDate(endOfDay(new Date()))
+  ]
+})
+const queryFormRef = ref() // 搜索的表单
+
+// 判断是否为复杂数据类型(struct 或 array)
+const isComplexDataType = computed(() => {
+  if (!thingModelDataType.value) return false
+  return [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY].includes(
+    thingModelDataType.value as any
+  )
+})
+
+// Echarts 数据
+const echartsData = computed(() => {
+  if (!list.value || list.value.length === 0) return []
+  return list.value.map((item) => [item.updateTime, item.value])
+})
+// Echarts 配置
+const echartsOption = reactive<any>({
+  title: {
+    text: '设备属性值',
+    left: 'center'
+  },
+  grid: {
+    left: 60,
+    right: 40,
+    bottom: 80,
+    top: 80,
+    containLabel: true
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    }
+  },
+  xAxis: {
+    type: 'time',
+    name: '时间',
+    axisLabel: {
+      formatter: (value: number) => formatDate(new Date(value), 'MM-DD HH:mm')
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '属性值'
+  },
+  series: [
+    {
+      name: '属性值',
+      type: 'line',
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 6,
+      lineStyle: {
+        width: 2,
+        color: '#1890FF'
+      },
+      itemStyle: {
+        color: '#1890FF'
+      },
+      data: []
+    }
+  ],
+  dataZoom: [
+    {
+      type: 'inside'
+    },
+    {
+      type: 'slider',
+      height: 30
+    }
+  ]
+})
+
+/** 获得设备历史数据 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DeviceApi.getHistoryDevicePropertyList(queryParams)
+    list.value = data || []
+    updateChartData()
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 打开弹窗 */
+const open = async (deviceId: number, identifier: string, dataType: string) => {
+  dialogVisible.value = true
+  queryParams.deviceId = deviceId
+  queryParams.identifier = identifier
+  thingModelDataType.value = dataType
+
+  // 如果物模型是 struct、array,需要默认使用 list 模式
+  if (isComplexDataType.value) {
+    viewMode.value = 'list'
+  } else {
+    viewMode.value = 'chart'
+  }
+  // 重置图表 key,确保每次打开都能正常渲染
+  chartKey.value = 0
+
+  // 等待弹窗完全渲染后再获取数据
+  await nextTick()
+  await getList()
+}
+
+/** 时间变化处理 */
+const handleTimeChange = () => {
+  getList()
+}
+
+/** 更新图表数据 */
+const updateChartData = () => {
+  if (echartsOption.series && echartsOption.series[0]) {
+    echartsOption.series[0].data = echartsData.value
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 208 - 0
src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue

@@ -0,0 +1,208 @@
+<!-- 设备服务调用 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+      @submit.prevent
+    >
+      <el-form-item label="标识符" prop="identifier">
+        <el-select
+          v-model="queryParams.identifier"
+          placeholder="请选择服务标识符"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="service in serviceThingModels"
+            :key="service.identifier"
+            :label="`${service.name}(${service.identifier})`"
+            :value="service.identifier!"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围" prop="times">
+        <el-date-picker
+          v-model="queryParams.times"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+          class="!w-360px"
+          :shortcuts="defaultShortcuts"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <!-- 服务调用列表 -->
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="调用时间" align="center" prop="requestTime" width="180px">
+        <template #default="scope">
+          {{ scope.row.request?.reportTime ? formatDate(scope.row.request.reportTime) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="响应时间" align="center" prop="responseTime" width="180px">
+        <template #default="scope">
+          {{ scope.row.reply?.reportTime ? formatDate(scope.row.reply.reportTime) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="标识符" align="center" prop="identifier" width="160px">
+        <template #default="scope">
+          <el-tag type="primary" size="small">
+            {{ scope.row.request?.identifier }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="服务名称" align="center" prop="serviceName" width="160px">
+        <template #default="scope">
+          {{ getServiceName(scope.row.request?.identifier) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="调用方式" align="center" prop="callType" width="100px">
+        <template #default="scope">
+          {{ getCallType(scope.row.request?.identifier) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="输入参数" align="center" prop="inputParams">
+        <template #default="scope"> {{ parseParams(scope.row.request?.params) }} </template>
+      </el-table-column>
+      <el-table-column label="输出参数" align="center" prop="outputParams">
+        <template #default="scope">
+          <span v-if="scope.row.reply">
+            {{
+              `{"code":${scope.row.reply.code},"msg":"${scope.row.reply.msg}","data":${scope.row.reply.data}\}`
+            }}
+          </span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelData } from '@/api/iot/thingmodel'
+import { formatDate, defaultShortcuts } from '@/utils/formatTime'
+import {
+  getThingModelServiceCallTypeLabel,
+  IotDeviceMessageMethodEnum,
+  IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+  deviceId: number
+  thingModelList: ThingModelData[]
+}>()
+
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([] as any[]) // 列表的数据
+const queryParams = reactive({
+  deviceId: props.deviceId,
+  method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, // 固定筛选服务调用消息
+  identifier: '',
+  times: [] as any[],
+  pageNo: 1,
+  pageSize: 10
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 服务类型的物模型数据 */
+const serviceThingModels = computed(() => {
+  return props.thingModelList.filter(
+    (item: ThingModelData) => item.type === IoTThingModelTypeEnum.SERVICE
+  )
+})
+
+/** 查询列表 */
+const getList = async () => {
+  if (!props.deviceId) return
+  loading.value = true
+  try {
+    const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
+    list.value = data.list
+    total.value = data.length
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.identifier = ''
+  queryParams.times = []
+  handleQuery()
+}
+
+/** 获取服务名称 */
+const getServiceName = (identifier: string | undefined) => {
+  if (!identifier) return '-'
+  const service = serviceThingModels.value.find(
+    (item: ThingModelData) => item.identifier === identifier
+  )
+  return service?.name || identifier
+}
+
+/** 获取调用方式 */
+const getCallType = (identifier: string | undefined) => {
+  if (!identifier) return '-'
+  const service = serviceThingModels.value.find(
+    (item: ThingModelData) => item.identifier === identifier
+  )
+  if (!service?.service?.callType) return '-'
+  return getThingModelServiceCallTypeLabel(service.service.callType) || '-'
+}
+
+/** 解析参数 */
+const parseParams = (params: string) => {
+  if (!params) return '-'
+  try {
+    const parsed = JSON.parse(params)
+    if (parsed.params) {
+      return JSON.stringify(parsed.params, null, 2)
+    }
+    return JSON.stringify(parsed, null, 2)
+  } catch (error) {
+    return params
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 30 - 10
src/views/iot/device/device/detail/index.vue

@@ -3,27 +3,30 @@
     :loading="loading"
     :product="product"
     :device="device"
-    @refresh="getDeviceData(id)"
+    @refresh="getDeviceData"
   />
   <el-col>
     <el-tabs v-model="activeTab">
       <el-tab-pane label="设备信息" name="info">
         <DeviceDetailsInfo v-if="activeTab === 'info'" :product="product" :device="device" />
       </el-tab-pane>
-      <el-tab-pane label="Topic 列表" />
       <el-tab-pane label="物模型数据" name="model">
-        <DeviceDetailsModel v-if="activeTab === 'model'" :product="product" :device="device" />
+        <DeviceDetailsThingModel
+          v-if="activeTab === 'model'"
+          :device-id="device.id"
+          :thing-model-list="thingModelList"
+        />
       </el-tab-pane>
       <el-tab-pane label="子设备管理" v-if="product.deviceType === DeviceTypeEnum.GATEWAY" />
-      <el-tab-pane label="设备影子" />
-      <el-tab-pane label="设备日志" name="log">
-        <DeviceDetailsLog v-if="activeTab === 'log'" :device-key="device.deviceKey" />
+      <el-tab-pane label="设备消息" name="log">
+        <DeviceDetailsMessage v-if="activeTab === 'log'" :device-id="device.id" />
       </el-tab-pane>
       <el-tab-pane label="模拟设备" name="simulator">
         <DeviceDetailsSimulator
           v-if="activeTab === 'simulator'"
           :product="product"
           :device="device"
+          :thing-model-list="thingModelList"
         />
       </el-tab-pane>
       <el-tab-pane label="设备配置" name="config">
@@ -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<ProductVO>({} as ProductVO) // 产品详情
 const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
 const activeTab = ref('info') // 默认激活的标签页
+const thingModelList = ref<ThingModelData[]>([]) // 物模型列表数据
 
 /** 获取设备详情 */
 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('参数错误,产品不能为空!')

+ 12 - 5
src/views/iot/device/device/index.vue

@@ -199,20 +199,20 @@
                 <div class="flex-1">
                   <div class="mb-2.5 last:mb-0">
                     <span class="text-[#717c8e] mr-2.5">所属产品</span>
-                    <span class="text-[#0070ff]">
+                    <el-link class="text-[#0070ff]" @click="openProductDetail(item.productId)">
                       {{ products.find((p) => p.id === item.productId)?.name }}
-                    </span>
+                    </el-link>
                   </div>
                   <div class="mb-2.5 last:mb-0">
                     <span class="text-[#717c8e] mr-2.5">设备类型</span>
                     <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="item.deviceType" />
                   </div>
                   <div class="mb-2.5 last:mb-0">
-                    <span class="text-[#717c8e] mr-2.5">DeviceKey</span>
+                    <span class="text-[#717c8e] mr-2.5">备注名称</span>
                     <span
                       class="text-[#0b1d30] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[130px]"
                     >
-                      {{ item.deviceKey }}
+                      {{ item.nickname || item.deviceName }}
                     </span>
                   </div>
                 </div>
@@ -289,7 +289,9 @@
       <el-table-column label="备注名称" align="center" prop="nickname" />
       <el-table-column label="所属产品" align="center" prop="productId">
         <template #default="scope">
-          {{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
+          <el-link @click="openProductDetail(scope.row.productId)">
+            {{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
+          </el-link>
         </template>
       </el-table-column>
       <el-table-column label="设备类型" align="center" prop="deviceType">
@@ -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 {

+ 50 - 0
src/views/iot/home/components/ComparisonCard.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-card class="stat-card" shadow="never" :loading="loading">
+    <div class="flex flex-col">
+      <div class="flex justify-between items-center mb-1">
+        <span class="text-gray-500 text-base font-medium">{{ title }}</span>
+        <Icon :icon="icon" :class="`text-[32px] ${iconColor}`" />
+      </div>
+      <span class="text-3xl font-bold text-gray-700">
+        <span v-if="value === -1">--</span>
+        <span v-else>{{ value }}</span>
+      </span>
+      <el-divider class="my-2" />
+      <div class="flex justify-between items-center text-gray-400 text-sm">
+        <span>今日新增</span>
+        <span class="text-green-500" v-if="todayCount !== -1">+{{ todayCount }}</span>
+        <span v-else>--</span>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+/** 【总数 + 新增数】统计卡片组件 */
+defineOptions({ name: 'IoTComparisonCard' })
+
+const props = defineProps({
+  title: propTypes.string.def('').isRequired,
+  value: propTypes.number.def(0).isRequired,
+  todayCount: propTypes.number.def(0).isRequired,
+  icon: propTypes.string.def('').isRequired,
+  iconColor: propTypes.string.def(''),
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.stat-card {
+  transition: all 0.3s;
+
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 5px 15px rgb(0 0 0 / 8%);
+  }
+}
+</style>

+ 131 - 0
src/views/iot/home/components/DeviceCountCard.vue

@@ -0,0 +1,131 @@
+<template>
+  <el-card class="chart-card" shadow="never" :loading="loading">
+    <template #header>
+      <div class="flex items-center">
+        <span class="text-base font-medium text-gray-600">设备数量统计</span>
+      </div>
+    </template>
+    <div v-if="loading && !hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="加载中..." />
+    </div>
+    <div v-else-if="!hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="暂无数据" />
+    </div>
+    <div v-else ref="deviceCountChartRef" class="h-[240px]"></div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { PieChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { TooltipComponent, LegendComponent } from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
+import type { PropType } from 'vue'
+
+/** 【设备数量】统计卡片 */
+defineOptions({ name: 'DeviceCountCard' })
+
+const props = defineProps({
+  statsData: {
+    type: Object as PropType<IotStatisticsSummaryRespVO>,
+    required: true
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const deviceCountChartRef = ref()
+
+/** 是否有数据 */
+const hasData = computed(() => {
+  if (!props.statsData) return false
+
+  const categories = Object.entries(props.statsData.productCategoryDeviceCounts || {})
+  return categories.length > 0 && props.statsData.deviceCount !== -1
+})
+
+/** 初始化图表 */
+const initChart = () => {
+  // 如果没有数据,则不初始化图表
+  if (!hasData.value) return
+  // 确保 DOM 元素存在且已渲染
+  if (!deviceCountChartRef.value) {
+    console.warn('图表DOM元素不存在')
+    return
+  }
+
+  echarts.use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer, LabelLayout])
+  try {
+    const chart = echarts.init(deviceCountChartRef.value)
+    chart.setOption({
+      tooltip: {
+        trigger: 'item'
+      },
+      legend: {
+        top: '5%',
+        right: '10%',
+        align: 'left',
+        orient: 'vertical',
+        icon: 'circle'
+      },
+      series: [
+        {
+          name: 'Access From',
+          type: 'pie',
+          radius: ['50%', '80%'],
+          avoidLabelOverlap: false,
+          center: ['30%', '50%'],
+          label: {
+            show: false,
+            position: 'outside'
+          },
+          emphasis: {
+            label: {
+              show: true,
+              fontSize: 20,
+              fontWeight: 'bold'
+            }
+          },
+          labelLine: {
+            show: false
+          },
+          data: Object.entries(props.statsData.productCategoryDeviceCounts).map(
+            ([name, value]) => ({
+              name,
+              value
+            })
+          )
+        }
+      ]
+    })
+    return chart
+  } catch (error) {
+    console.error('初始化图表失败:', error)
+    return null
+  }
+}
+
+/** 监听数据变化 */
+watch(
+  () => props.statsData,
+  () => {
+    // 使用 nextTick 确保 DOM 已更新
+    nextTick(() => {
+      initChart()
+    })
+  },
+  { deep: true }
+)
+
+/** 组件挂载时初始化图表 */
+onMounted(async () => {
+  // 使用 nextTick 确保 DOM 已更新
+  await nextTick(() => {
+    initChart()
+  })
+})
+</script>

+ 163 - 0
src/views/iot/home/components/DeviceStateCountCard.vue

@@ -0,0 +1,163 @@
+<template>
+  <el-card class="chart-card" shadow="never" :loading="loading">
+    <template #header>
+      <div class="flex items-center">
+        <span class="text-base font-medium text-gray-600">设备状态统计</span>
+      </div>
+    </template>
+    <div v-if="loading && !hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="加载中..." />
+    </div>
+    <div v-else-if="!hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="暂无数据" />
+    </div>
+    <el-row v-else class="h-[240px]">
+      <el-col :span="8" class="flex flex-col items-center">
+        <div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
+        <div class="text-center mt-2">
+          <span class="text-sm text-gray-600">在线设备</span>
+        </div>
+      </el-col>
+      <el-col :span="8" class="flex flex-col items-center">
+        <div ref="deviceOfflineChartRef" class="h-[160px] w-full"></div>
+        <div class="text-center mt-2">
+          <span class="text-sm text-gray-600">离线设备</span>
+        </div>
+      </el-col>
+      <el-col :span="8" class="flex flex-col items-center">
+        <div ref="deviceActiveChartRef" class="h-[160px] w-full"></div>
+        <div class="text-center mt-2">
+          <span class="text-sm text-gray-600">待激活设备</span>
+        </div>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { GaugeChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
+import type { PropType } from 'vue'
+
+/** 【设备状态】统计卡片 */
+defineOptions({ name: 'DeviceStateCountCard' })
+
+const props = defineProps({
+  statsData: {
+    type: Object as PropType<IotStatisticsSummaryRespVO>,
+    required: true
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const deviceOnlineCountChartRef = ref()
+const deviceOfflineChartRef = ref()
+const deviceActiveChartRef = ref()
+
+/** 是否有数据 */
+const hasData = computed(() => {
+  if (!props.statsData) return false
+  return props.statsData.deviceCount !== -1
+})
+
+/** 初始化仪表盘图表 */
+const initGaugeChart = (el: any, value: number, color: string) => {
+  // 确保 DOM 元素存在且已渲染
+  if (!el) {
+    console.warn('图表DOM元素不存在')
+    return
+  }
+
+  echarts.use([GaugeChart, CanvasRenderer])
+  try {
+    const chart = echarts.init(el)
+    chart.setOption({
+      series: [
+        {
+          type: 'gauge',
+          startAngle: 360,
+          endAngle: 0,
+          min: 0,
+          max: props.statsData.deviceCount || 100, // 使用设备总数作为最大值
+          progress: {
+            show: true,
+            width: 12,
+            itemStyle: {
+              color: color
+            }
+          },
+          axisLine: {
+            lineStyle: {
+              width: 12,
+              color: [[1, '#E5E7EB']]
+            }
+          },
+          axisTick: { show: false },
+          splitLine: { show: false },
+          axisLabel: { show: false },
+          pointer: { show: false },
+          anchor: { show: false },
+          title: { show: false },
+          detail: {
+            valueAnimation: true,
+            fontSize: 24,
+            fontWeight: 'bold',
+            fontFamily: 'Inter, sans-serif',
+            color: color,
+            offsetCenter: [0, '0'],
+            formatter: (value: number) => {
+              return `${value} 个`
+            }
+          },
+          data: [{ value: value }]
+        }
+      ]
+    })
+    return chart
+  } catch (error) {
+    console.error('初始化图表失败:', error)
+    return null
+  }
+}
+
+/** 初始化所有图表 */
+const initCharts = () => {
+  // 如果没有数据,则不初始化图表
+  if (!hasData.value) return
+
+  // 使用 nextTick 确保 DOM 已更新
+  nextTick(() => {
+    // 在线设备统计
+    if (deviceOnlineCountChartRef.value) {
+      initGaugeChart(deviceOnlineCountChartRef.value, props.statsData.deviceOnlineCount, '#0d9')
+    }
+    // 离线设备统计
+    if (deviceOfflineChartRef.value) {
+      initGaugeChart(deviceOfflineChartRef.value, props.statsData.deviceOfflineCount, '#f50')
+    }
+    // 待激活设备统计
+    if (deviceActiveChartRef.value) {
+      initGaugeChart(deviceActiveChartRef.value, props.statsData.deviceInactiveCount, '#05b')
+    }
+  })
+}
+
+/** 监听数据变化 */
+watch(
+  () => props.statsData,
+  () => {
+    initCharts()
+  },
+  { deep: true }
+)
+
+/** 组件挂载时初始化图表 */
+onMounted(() => {
+  initCharts()
+})
+</script>

+ 227 - 0
src/views/iot/home/components/MessageTrendCard.vue

@@ -0,0 +1,227 @@
+<template>
+  <el-card class="chart-card" shadow="never" :loading="loading">
+    <template #header>
+      <div class="flex items-center justify-between">
+        <span class="text-base font-medium text-gray-600">消息量统计</span>
+        <div class="flex flex-wrap items-center gap-4">
+          <el-form-item label="时间范围" class="!mb-0">
+            <el-date-picker
+              v-model="queryParams.times"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              :shortcuts="defaultShortcuts"
+              class="!w-240px"
+              end-placeholder="结束日期"
+              start-placeholder="开始日期"
+              type="daterange"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              @change="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="时间间隔" class="!mb-0">
+            <el-select
+              v-model="queryParams.interval"
+              class="!w-120px"
+              placeholder="间隔类型"
+              @change="handleQuery"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+      </div>
+    </template>
+    <div v-if="loading && !hasData" class="h-[300px] flex justify-center items-center">
+      <el-empty description="加载中..." />
+    </div>
+    <div v-else-if="!hasData" class="h-[300px] flex justify-center items-center">
+      <el-empty description="暂无数据" />
+    </div>
+    <div v-else ref="messageChartRef" class="h-[300px]"></div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { LineChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
+import { UniversalTransition } from 'echarts/features'
+import {
+  StatisticsApi,
+  IotStatisticsDeviceMessageSummaryByDateRespVO,
+  IotStatisticsDeviceMessageReqVO
+} from '@/api/iot/statistics'
+import { formatDate, beginOfDay, endOfDay, defaultShortcuts } from '@/utils/formatTime'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+/** 消息趋势统计卡片 */
+defineOptions({ name: 'MessageTrendCard' })
+
+const messageChartRef = ref()
+const loading = ref(false)
+const messageData = ref<IotStatisticsDeviceMessageSummaryByDateRespVO[]>([])
+
+const queryParams = reactive<IotStatisticsDeviceMessageReqVO>({
+  interval: 1, // DAY, 日
+  times: [
+    // 默认显示最近一周的数据
+    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+    formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+  ]
+}) // 查询参数
+
+// 是否有数据
+const hasData = computed(() => {
+  return messageData.value && messageData.value.length > 0
+})
+
+// 处理查询操作
+const handleQuery = () => {
+  fetchMessageData()
+}
+
+// 获取消息统计数据
+const fetchMessageData = async () => {
+  loading.value = true
+  try {
+    messageData.value = await StatisticsApi.getDeviceMessageSummaryByDate(queryParams)
+
+    // 使用 nextTick 确保数据更新后重新渲染图表
+    await nextTick()
+    initChart()
+  } catch (error) {
+    console.error('获取消息统计数据失败:', error)
+    messageData.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+// 初始化图表
+const initChart = () => {
+  // 检查是否有数据可以绘制
+  if (!hasData.value) return
+  // 确保 DOM 元素存在且已渲染
+  if (!messageChartRef.value) {
+    console.warn('图表 DOM 元素不存在')
+    return
+  }
+
+  // 配置图表
+  echarts.use([
+    LineChart,
+    CanvasRenderer,
+    GridComponent,
+    LegendComponent,
+    TooltipComponent,
+    UniversalTransition
+  ])
+  try {
+    const chart = echarts.init(messageChartRef.value)
+    chart.setOption({
+      tooltip: {
+        trigger: 'axis',
+        backgroundColor: 'rgba(255, 255, 255, 0.9)',
+        borderColor: '#E5E7EB',
+        textStyle: {
+          color: '#374151'
+        }
+      },
+      legend: {
+        data: ['上行消息量', '下行消息量'],
+        textStyle: {
+          color: '#374151',
+          fontWeight: 500
+        }
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: messageData.value.map((item) => item.time),
+        axisLine: {
+          lineStyle: {
+            color: '#E5E7EB'
+          }
+        },
+        axisLabel: {
+          color: '#6B7280'
+        }
+      },
+      yAxis: {
+        type: 'value',
+        axisLine: {
+          lineStyle: {
+            color: '#E5E7EB'
+          }
+        },
+        axisLabel: {
+          color: '#6B7280'
+        },
+        splitLine: {
+          lineStyle: {
+            color: '#F3F4F6'
+          }
+        }
+      },
+      series: [
+        {
+          name: '上行消息量',
+          type: 'line',
+          smooth: true,
+          data: messageData.value.map((item) => item.upstreamCount),
+          itemStyle: {
+            color: '#3B82F6'
+          },
+          lineStyle: {
+            width: 2
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
+              { offset: 1, color: 'rgba(59, 130, 246, 0)' }
+            ])
+          }
+        },
+        {
+          name: '下行消息量',
+          type: 'line',
+          smooth: true,
+          data: messageData.value.map((item) => item.downstreamCount),
+          itemStyle: {
+            color: '#10B981'
+          },
+          lineStyle: {
+            width: 2
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
+              { offset: 1, color: 'rgba(16, 185, 129, 0)' }
+            ])
+          }
+        }
+      ]
+    })
+    return chart
+  } catch (error) {
+    console.error('初始化图表失败:', error)
+    return null
+  }
+}
+
+/** 组件挂载时初始化 */
+onMounted(() => {
+  fetchMessageData()
+})
+</script>

+ 62 - 461
src/views/iot/home/index.vue

@@ -2,145 +2,61 @@
   <!-- 第一行:统计卡片行 -->
   <el-row :gutter="16" class="mb-4">
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">分类数量</span>
-            <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">
-            {{ statsData.productCategoryCount }}
-          </span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="分类数量"
+        :value="statsData.productCategoryCount"
+        :todayCount="statsData.productCategoryTodayCount"
+        icon="ep:menu"
+        iconColor="text-blue-400"
+        :loading="loading"
+      />
     </el-col>
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">产品数量</span>
-            <Icon icon="ep:box" class="text-[32px] text-orange-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">{{ statsData.productCount }}</span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.productTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="产品数量"
+        :value="statsData.productCount"
+        :todayCount="statsData.productTodayCount"
+        icon="ep:box"
+        iconColor="text-orange-400"
+        :loading="loading"
+      />
     </el-col>
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">设备数量</span>
-            <Icon icon="ep:cpu" class="text-[32px] text-purple-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">{{ statsData.deviceCount }}</span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.deviceTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="设备数量"
+        :value="statsData.deviceCount"
+        :todayCount="statsData.deviceTodayCount"
+        icon="ep:cpu"
+        iconColor="text-purple-400"
+        :loading="loading"
+      />
     </el-col>
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">设备消息数</span>
-            <Icon icon="ep:message" class="text-[32px] text-teal-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">
-            {{ statsData.deviceMessageCount }}
-          </span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.deviceMessageTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="设备消息数"
+        :value="statsData.deviceMessageCount"
+        :todayCount="statsData.deviceMessageTodayCount"
+        icon="ep:message"
+        iconColor="text-teal-400"
+        :loading="loading"
+      />
     </el-col>
   </el-row>
 
   <!-- 第二行:图表行 -->
   <el-row :gutter="16" class="mb-4">
     <el-col :span="12">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">设备数量统计</span>
-          </div>
-        </template>
-        <div ref="deviceCountChartRef" class="h-[240px]"></div>
-      </el-card>
+      <DeviceCountCard :statsData="statsData" :loading="loading" />
     </el-col>
     <el-col :span="12">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">设备状态统计</span>
-          </div>
-        </template>
-        <el-row class="h-[240px]">
-          <el-col :span="8" class="flex flex-col items-center">
-            <div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">在线设备</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="flex flex-col items-center">
-            <div ref="deviceOfflineChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">离线设备</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="flex flex-col items-center">
-            <div ref="deviceActiveChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">待激活设备</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
+      <DeviceStateCountCard :statsData="statsData" :loading="loading" />
     </el-col>
   </el-row>
 
   <!-- 第三行:消息统计行 -->
   <el-row>
     <el-col :span="24">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center justify-between">
-            <span class="text-base font-medium text-gray-600">上下行消息量统计</span>
-            <div class="flex items-center space-x-2">
-              <el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
-                <el-radio-button label="1h">最近1小时</el-radio-button>
-                <el-radio-button label="24h">最近24小时</el-radio-button>
-                <el-radio-button label="7d">近一周</el-radio-button>
-              </el-radio-group>
-              <el-date-picker
-                v-model="dateRange"
-                type="datetimerange"
-                range-separator="至"
-                start-placeholder="开始时间"
-                end-placeholder="结束时间"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-                @change="handleDateRangeChange"
-              />
-            </div>
-          </div>
-        </template>
-        <div ref="deviceMessageCountChartRef" class="h-[300px]"></div>
-      </el-card>
+      <MessageTrendCard />
     </el-col>
   </el-row>
 
@@ -148,356 +64,43 @@
 </template>
 
 <script setup lang="ts" name="Index">
-import * as echarts from 'echarts/core'
-import {
-  GridComponent,
-  LegendComponent,
-  TitleComponent,
-  ToolboxComponent,
-  TooltipComponent
-} from 'echarts/components'
-import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
-import { LabelLayout, UniversalTransition } from 'echarts/features'
-import { CanvasRenderer } from 'echarts/renderers'
-import {
-  IotStatisticsDeviceMessageSummaryRespVO,
-  IotStatisticsSummaryRespVO,
-  ProductCategoryApi
-} from '@/api/iot/statistics'
-import { formatDate } from '@/utils/formatTime'
-
-// TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
+import { IotStatisticsSummaryRespVO, StatisticsApi } from '@/api/iot/statistics'
+import ComparisonCard from './components/ComparisonCard.vue'
+import DeviceCountCard from './components/DeviceCountCard.vue'
+import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
+import MessageTrendCard from './components/MessageTrendCard.vue'
 
 /** IoT 首页 */
 defineOptions({ name: 'IoTHome' })
 
-// TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
-echarts.use([
-  TooltipComponent,
-  LegendComponent,
-  PieChart,
-  CanvasRenderer,
-  LabelLayout,
-  TitleComponent,
-  ToolboxComponent,
-  GridComponent,
-  LineChart,
-  UniversalTransition,
-  GaugeChart
-])
-
-const timeRange = ref('7d') // 修改默认选择为近一周
-const dateRange = ref<[Date, Date] | null>(null)
-
-const queryParams = reactive({
-  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
-  endTime: Date.now() // 设置默认结束时间为当前时间
-})
-
-const deviceCountChartRef = ref() // 设备数量统计的图表
-const deviceOnlineCountChartRef = ref() // 在线设备统计的图表
-const deviceOfflineChartRef = ref() // 离线设备统计的图表
-const deviceActiveChartRef = ref() // 待激活设备统计的图表
-const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
-
-// 基础统计数据
-// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
 const statsData = ref<IotStatisticsSummaryRespVO>({
-  productCategoryCount: 0,
-  productCount: 0,
-  deviceCount: 0,
-  deviceMessageCount: 0,
-  productCategoryTodayCount: 0,
-  productTodayCount: 0,
-  deviceTodayCount: 0,
-  deviceMessageTodayCount: 0,
-  deviceOnlineCount: 0,
-  deviceOfflineCount: 0,
-  deviceInactiveCount: 0,
+  productCategoryCount: -1,
+  productCount: -1,
+  deviceCount: -1,
+  deviceMessageCount: -1,
+  productCategoryTodayCount: -1,
+  productTodayCount: -1,
+  deviceTodayCount: -1,
+  deviceMessageTodayCount: -1,
+  deviceOnlineCount: -1,
+  deviceOfflineCount: -1,
+  deviceInactiveCount: -1,
   productCategoryDeviceCounts: {}
-})
-
-// 消息统计数据
-const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
-  upstreamCounts: {},
-  downstreamCounts: {}
-})
-
-/** 处理快捷时间范围选择 */
-const handleTimeRangeChange = (timeRange: string) => {
-  const now = Date.now()
-  let startTime: number
-
-  // TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
-  switch (timeRange) {
-    case '1h':
-      startTime = now - 60 * 60 * 1000
-      break
-    case '24h':
-      startTime = now - 24 * 60 * 60 * 1000
-      break
-    case '7d':
-      startTime = now - 7 * 24 * 60 * 60 * 1000
-      break
-    default:
-      return
-  }
-
-  // 清空日期选择器
-  dateRange.value = null
-
-  // 更新查询参数
-  queryParams.startTime = startTime
-  queryParams.endTime = now
-
-  // 重新获取数据
-  getStats()
-}
-
-/** 处理自定义日期范围选择 */
-const handleDateRangeChange = (value: [Date, Date] | null) => {
-  if (value) {
-    // 清空快捷选项
-    timeRange.value = ''
+}) // 基础统计数据
 
-    // 更新查询参数
-    queryParams.startTime = value[0].getTime()
-    queryParams.endTime = value[1].getTime()
-
-    // 重新获取数据
-    getStats()
-  }
-}
+const loading = ref(true) // 加载状态
 
 /** 获取统计数据 */
 const getStats = async () => {
-  // 获取基础统计数据
-  statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
-
-  // 获取消息统计数据
-  messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
-
-  // 初始化图表
-  initCharts()
-}
-
-/** 初始化图表 */
-const initCharts = () => {
-  // 设备数量统计
-  echarts.init(deviceCountChartRef.value).setOption({
-    tooltip: {
-      trigger: 'item'
-    },
-    legend: {
-      top: '5%',
-      right: '10%',
-      align: 'left',
-      orient: 'vertical',
-      icon: 'circle'
-    },
-    series: [
-      {
-        name: 'Access From',
-        type: 'pie',
-        radius: ['50%', '80%'],
-        avoidLabelOverlap: false,
-        center: ['30%', '50%'],
-        label: {
-          show: false,
-          position: 'outside'
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: 20,
-            fontWeight: 'bold'
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: Object.entries(statsData.value.productCategoryDeviceCounts).map(([name, value]) => ({
-          name,
-          value
-        }))
-      }
-    ]
-  })
-
-  // 在线设备统计
-  initGaugeChart(deviceOnlineCountChartRef.value, statsData.value.deviceOnlineCount, '#0d9')
-  // 离线设备统计
-  initGaugeChart(deviceOfflineChartRef.value, statsData.value.deviceOfflineCount, '#f50')
-  // 待激活设备统计
-  initGaugeChart(deviceActiveChartRef.value, statsData.value.deviceInactiveCount, '#05b')
-
-  // 消息量统计
-  initMessageChart()
-}
-
-/** 初始化仪表盘图表 */
-const initGaugeChart = (el: any, value: number, color: string) => {
-  echarts.init(el).setOption({
-    series: [
-      {
-        type: 'gauge',
-        startAngle: 360,
-        endAngle: 0,
-        min: 0,
-        max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
-        progress: {
-          show: true,
-          width: 12,
-          itemStyle: {
-            color: color
-          }
-        },
-        axisLine: {
-          lineStyle: {
-            width: 12,
-            color: [[1, '#E5E7EB']]
-          }
-        },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        pointer: { show: false },
-        anchor: { show: false },
-        title: { show: false },
-        detail: {
-          valueAnimation: true,
-          fontSize: 24,
-          fontWeight: 'bold',
-          fontFamily: 'Inter, sans-serif',
-          color: color,
-          offsetCenter: [0, '0'],
-          formatter: (value: number) => {
-            return `${value} 个`
-          }
-        },
-        data: [{ value: value }]
-      }
-    ]
-  })
-}
-
-/** 初始化消息统计图表 */
-const initMessageChart = () => {
-  // 获取所有时间戳并排序
-  // TODO @super:一些 idea 里的红色报错,要去处理掉噢。
-  const timestamps = Array.from(
-    new Set([
-      ...messageStats.value.upstreamCounts.map((item) => Number(Object.keys(item)[0])),
-      ...messageStats.value.downstreamCounts.map((item) => Number(Object.keys(item)[0]))
-    ])
-  ).sort((a, b) => a - b) // 确保时间戳从小到大排序
-
-  // 准备数据
-  const xdata = timestamps.map((ts) => formatDate(ts, 'YYYY-MM-DD HH:mm'))
-  const upData = timestamps.map((ts) => {
-    const item = messageStats.value.upstreamCounts.find(
-      (count) => Number(Object.keys(count)[0]) === ts
-    )
-    return item ? Object.values(item)[0] : 0
-  })
-  const downData = timestamps.map((ts) => {
-    const item = messageStats.value.downstreamCounts.find(
-      (count) => Number(Object.keys(count)[0]) === ts
-    )
-    return item ? Object.values(item)[0] : 0
-  })
-
-  // 配置图表
-  echarts.init(deviceMessageCountChartRef.value).setOption({
-    tooltip: {
-      trigger: 'axis',
-      backgroundColor: 'rgba(255, 255, 255, 0.9)',
-      borderColor: '#E5E7EB',
-      textStyle: {
-        color: '#374151'
-      }
-    },
-    legend: {
-      data: ['上行消息量', '下行消息量'],
-      textStyle: {
-        color: '#374151',
-        fontWeight: 500
-      }
-    },
-    grid: {
-      left: '3%',
-      right: '4%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: xdata,
-      axisLine: {
-        lineStyle: {
-          color: '#E5E7EB'
-        }
-      },
-      axisLabel: {
-        color: '#6B7280'
-      }
-    },
-    yAxis: {
-      type: 'value',
-      axisLine: {
-        lineStyle: {
-          color: '#E5E7EB'
-        }
-      },
-      axisLabel: {
-        color: '#6B7280'
-      },
-      splitLine: {
-        lineStyle: {
-          color: '#F3F4F6'
-        }
-      }
-    },
-    series: [
-      {
-        name: '上行消息量',
-        type: 'line',
-        smooth: true, // 添加平滑曲线
-        data: upData,
-        itemStyle: {
-          color: '#3B82F6'
-        },
-        lineStyle: {
-          width: 2
-        },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
-            { offset: 1, color: 'rgba(59, 130, 246, 0)' }
-          ])
-        }
-      },
-      {
-        name: '下行消息量',
-        type: 'line',
-        smooth: true, // 添加平滑曲线
-        data: downData,
-        itemStyle: {
-          color: '#10B981'
-        },
-        lineStyle: {
-          width: 2
-        },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
-            { offset: 1, color: 'rgba(16, 185, 129, 0)' }
-          ])
-        }
-      }
-    ]
-  })
+  loading.value = true
+  try {
+    // 获取基础统计数据
+    statsData.value = await StatisticsApi.getStatisticsSummary()
+  } catch (error) {
+    console.error('获取统计数据出错:', error)
+  } finally {
+    loading.value = false
+  }
 }
 
 /** 初始化 */
@@ -505,5 +108,3 @@ onMounted(() => {
   getStats()
 })
 </script>
-
-<style lang="scss" scoped></style>

+ 169 - 0
src/views/iot/ota/firmware/OtaFirmwareForm.vue

@@ -0,0 +1,169 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="固件名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入固件名称" />
+      </el-form-item>
+      <el-form-item label="固件描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          :rows="3"
+          placeholder="请输入固件描述"
+        />
+      </el-form-item>
+      <el-form-item label="所属产品" prop="productId">
+        <el-select
+          v-model="formData.productId"
+          placeholder="请选择产品"
+          clearable
+          class="!w-100%"
+          :disabled="formType === 'update'"
+        >
+          <el-option
+            v-for="product in productList"
+            :key="product.id"
+            :label="product.name"
+            :value="product.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="版本号" prop="version" v-if="formType === 'create'">
+        <el-input v-model="formData.version" placeholder="请输入版本号" />
+      </el-form-item>
+      <el-form-item label="固件文件" prop="fileUrl" v-if="formType === 'create'">
+        <UploadFile
+          v-model="formData.fileUrl"
+          :file-type="['bin', 'zip', 'pdf']"
+          :file-size="50"
+          :limit="1"
+        />
+      </el-form-item>
+      <!-- 更新时显示只读信息 -->
+      <template v-if="formType === 'update'">
+        <el-form-item label="版本号">
+          <el-input v-model="formData.version" readonly />
+        </el-form-item>
+        <el-form-item label="固件文件">
+          <el-link
+            type="primary"
+            :href="formData.fileUrl"
+            target="_blank"
+            download
+            v-if="formData.fileUrl"
+          >
+            <Icon icon="ep:download" class="mr-5px" />
+            下载固件文件
+          </el-link>
+          <span v-else>无文件</span>
+        </el-form-item>
+      </template>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { UploadFile } from '@/components/UploadFile'
+
+/** IoT OTA 固件表单 */
+defineOptions({ name: 'IoTOtaFirmwareForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const productList = ref<ProductVO[]>([]) // 产品列表
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  version: undefined,
+  productId: undefined,
+  fileUrl: ''
+})
+const formRules = reactive({
+  name: [{ required: true, message: '固件名称不能为空', trigger: 'blur' }],
+  version: [{ required: true, message: '版本号不能为空', trigger: 'blur' }],
+  productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
+  fileUrl: [{ required: true, message: '固件文件不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IoTOtaFirmwareApi.getOtaFirmware(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  // 获取产品列表
+  productList.value = await ProductApi.getSimpleProductList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as IoTOtaFirmware
+    if (formType.value === 'create') {
+      await IoTOtaFirmwareApi.createOtaFirmware(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      // 更新时只提交可编辑的字段
+      await IoTOtaFirmwareApi.updateOtaFirmware({
+        id: data.id,
+        name: data.name,
+        description: data.description
+      })
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    version: undefined,
+    productId: undefined,
+    fileUrl: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 143 - 0
src/views/iot/ota/firmware/detail/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="app-container">
+    <!-- 固件信息 -->
+    <ContentWrap title="固件信息" class="mb-20px">
+      <el-descriptions :column="3" v-loading="firmwareLoading" border>
+        <el-descriptions-item label="固件名称">
+          {{ firmware?.name }}
+        </el-descriptions-item>
+        <el-descriptions-item label="所属产品">
+          {{ firmware?.productName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="固件版本">
+          {{ firmware?.version }}
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间">
+          {{ firmware?.createTime ? formatDate(firmware.createTime) : '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="固件描述" :span="2">
+          {{ firmware?.description }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </ContentWrap>
+
+    <!-- 升级设备统计 -->
+    <ContentWrap title="升级设备统计" class="mb-20px">
+      <el-row :gutter="20" class="py-20px" v-loading="firmwareStatisticsLoading">
+        <el-col :span="6">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-blue-500">
+              {{
+                Object.values(firmwareStatistics).reduce((sum, count) => sum + (count || 0), 0) || 0
+              }}
+            </div>
+            <div class="text-14px text-gray-600">升级设备总数</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-gray-400">
+              {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">待推送</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-blue-400">
+              {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">已推送</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-yellow-500">
+              {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">正在升级</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-green-500">
+              {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级成功</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-red-500">
+              {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级失败</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-gray-400">
+              {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级取消</div>
+          </div>
+        </el-col>
+      </el-row>
+    </ContentWrap>
+
+    <!-- 任务管理 -->
+    <OtaTaskList
+      :firmware-id="firmwareId"
+      :product-id="firmware?.productId"
+      @success="getStatistics"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { formatDate } from '@/utils/formatTime'
+import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
+import { IoTOtaTaskRecordApi } from '@/api/iot/ota/task/record'
+import { IoTOtaTaskRecordStatusEnum } from '@/views/iot/utils/constants'
+import OtaTaskList from '../../task/OtaTaskList.vue'
+
+/** IoT OTA 固件详情 */
+defineOptions({ name: 'IoTOtaFirmwareDetail' })
+
+const route = useRoute() // 路由
+
+const firmwareId = ref(Number(route.params.id)) // 固件编号
+const firmwareLoading = ref(false) // 固件加载状态
+const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware) // 固件信息
+
+const firmwareStatisticsLoading = ref(false) // 统计信息加载状态
+const firmwareStatistics = ref<Record<string, number>>({}) // 统计信息
+
+/** 获取固件信息 */
+const getFirmwareInfo = async () => {
+  firmwareLoading.value = true
+  try {
+    firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value)
+  } finally {
+    firmwareLoading.value = false
+  }
+}
+
+/** 获取升级统计 */
+const getStatistics = async () => {
+  firmwareStatisticsLoading.value = true
+  try {
+    firmwareStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
+      firmwareId.value
+    )
+  } finally {
+    firmwareStatisticsLoading.value = false
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getFirmwareInfo()
+  getStatistics()
+})
+</script>

+ 232 - 0
src/views/iot/ota/firmware/index.vue

@@ -0,0 +1,232 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="固件名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入固件名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="产品" prop="productId">
+        <el-select
+          v-model="queryParams.productId"
+          placeholder="请选择产品"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="product in productList"
+            :key="product.id"
+            :label="product.name"
+            :value="product.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iot:ota-firmware:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      row-key="id"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+    >
+      <el-table-column label="固件编号" align="center" prop="id" />
+      <el-table-column label="固件名称" align="center" prop="name" />
+      <el-table-column label="固件版本" align="center" prop="description" />
+      <el-table-column label="版本号" align="center" prop="version" />
+      <el-table-column label="所属产品" align="center" prop="productId">
+        <template #default="scope">
+          <el-link
+            @click="openProductDetail(scope.row.productId)"
+            v-if="getProductName(scope.row.productId)"
+          >
+            {{ getProductName(scope.row.productId) }}
+          </el-link>
+          <span v-else>加载中...</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="固件文件" align="center" prop="fileUrl">
+        <template #default="scope">
+          <el-link :href="scope.row.fileUrl" target="_blank" download>
+            <Icon icon="ep:download" class="mr-5px" />
+            下载固件
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="180px">
+        <template #default="scope">
+          <el-button
+            link
+            @click="openFirmwareDetail(scope.row.id)"
+            v-hasPermi="['iot:ota-firmware:query']"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iot:ota-firmware:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iot:ota-firmware:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <OtaFirmwareForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import OtaFirmwareForm from './OtaFirmwareForm.vue'
+
+/** IoT OTA 固件列表 */
+defineOptions({ name: 'IoTOtaFirmware' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IoTOtaFirmware[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const productList = ref<ProductVO[]>([]) // 产品列表
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  productId: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IoTOtaFirmwareApi.getOtaFirmwarePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 根据产品编号,获取产品名称 */
+const getProductName = (productId: number) => {
+  const product = productList.value.find((p) => p.id === productId)
+  return product?.name || ''
+}
+
+/** 打开产品详情 */
+const openProductDetail = (productId: number) => {
+  push({ name: 'IoTProductDetail', params: { id: productId } })
+}
+
+/** 打开固件详情 */
+const openFirmwareDetail = (firmwareId: number) => {
+  push({ name: 'IoTOtaFirmwareDetail', params: { id: firmwareId } })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IoTOtaFirmwareApi.deleteOtaFirmware(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  productList.value = await ProductApi.getSimpleProductList()
+  getList()
+})
+</script>

+ 285 - 0
src/views/iot/ota/task/OtaTaskDetail.vue

@@ -0,0 +1,285 @@
+<template>
+  <Dialog v-model="dialogVisible" title="升级任务详情" width="1200px" append-to-body>
+    <!-- 任务信息 -->
+    <ContentWrap title="任务信息" class="mb-20px">
+      <el-descriptions :column="3" v-loading="taskLoading" border>
+        <el-descriptions-item label="任务编号">{{ task.id }}</el-descriptions-item>
+        <el-descriptions-item label="任务名称">{{ task.name }}</el-descriptions-item>
+        <el-descriptions-item label="升级范围">
+          <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE" :value="task.deviceScope" />
+        </el-descriptions-item>
+        <el-descriptions-item label="任务状态">
+          <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_STATUS" :value="task.status" />
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间">
+          {{ task.createTime ? formatDate(task.createTime) : '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="任务描述" :span="3">
+          {{ task.description }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </ContentWrap>
+
+    <!-- 任务升级设备统计 -->
+    <ContentWrap title="升级设备统计" class="mb-20px">
+      <el-row :gutter="20" class="py-20px" v-loading="taskStatisticsLoading">
+        <el-col :span="6">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-blue-500">
+              {{ Object.values(taskStatistics).reduce((sum, count) => sum + (count || 0), 0) || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级设备总数</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-gray-400">
+              {{ taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">待推送</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-blue-400">
+              {{ taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">已推送</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-yellow-500">
+              {{ taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">正在升级</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-green-500">
+              {{ taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级成功</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-red-500">
+              {{ taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级失败</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+            <div class="text-32px font-bold mb-8px text-gray-400">
+              {{ taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
+            </div>
+            <div class="text-14px text-gray-600">升级取消</div>
+          </div>
+        </el-col>
+      </el-row>
+    </ContentWrap>
+
+    <!-- 设备管理 -->
+    <ContentWrap title="升级设备记录">
+      <!-- Tab 切换 -->
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-15px">
+        <el-tab-pane v-for="tab in statusTabs" :key="tab.key" :label="tab.label" :name="tab.key" />
+      </el-tabs>
+      <!-- Tab 内容 -->
+      <div v-for="tab in statusTabs" :key="tab.key" v-show="activeTab === tab.key">
+        <!-- 设备列表 -->
+        <el-table
+          v-loading="recordLoading"
+          :data="recordList"
+          :stripe="true"
+          :show-overflow-tooltip="true"
+        >
+          <el-table-column label="设备名称" align="center" prop="deviceName" />
+          <el-table-column label="当前版本" align="center" prop="fromFirmwareVersion" />
+          <el-table-column label="升级状态" align="center" prop="status" width="120">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_RECORD_STATUS" :value="scope.row.status" />
+            </template>
+          </el-table-column>
+          <el-table-column label="升级进度" align="center" prop="progress" width="120">
+            <template #default="scope"> {{ scope.row.progress }}% </template>
+          </el-table-column>
+          <el-table-column label="状态描述" align="center" prop="description" />
+          <el-table-column label="更新时间" align="center" prop="updateTime" width="180">
+            <template #default="scope">
+              {{ formatDate(scope.row.updateTime) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" width="80">
+            <template #default="scope">
+              <el-button
+                v-if="
+                  [
+                    IoTOtaTaskRecordStatusEnum.PENDING.value,
+                    IoTOtaTaskRecordStatusEnum.PUSHED.value,
+                    IoTOtaTaskRecordStatusEnum.UPGRADING.value
+                  ].includes(scope.row.status)
+                "
+                link
+                type="danger"
+                @click="handleCancelUpgrade(scope.row)"
+                v-hasPermi="['iot:ota-task-record:cancel']"
+              >
+                取消
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="recordTotal"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getRecordList"
+        />
+      </div>
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { TabsPaneContext } from 'element-plus'
+import { Dialog } from '@/components/Dialog'
+import { ContentWrap } from '@/components/ContentWrap'
+import Pagination from '@/components/Pagination/index.vue'
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { IoTOtaTaskRecordApi, OtaTaskRecord } from '@/api/iot/ota/task/record'
+import { DICT_TYPE } from '@/utils/dict'
+import { IoTOtaTaskRecordStatusEnum } from '@/views/iot/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+
+/** OTA 任务详情组件 */
+defineOptions({ name: 'OtaTaskDetail' })
+
+const message = useMessage() // 消息弹窗
+const dialogVisible = ref(false) // 弹窗的是否展示
+
+const taskId = ref<number>() // 任务编号
+const taskLoading = ref(false) // 任务加载状态
+const task = ref<OtaTask>({} as OtaTask) // 任务信息
+
+const taskStatisticsLoading = ref(false) // 任务统计加载状态
+const taskStatistics = ref<Record<string, number>>({}) // 任务统计数据
+
+const recordLoading = ref(false) // 记录列表加载状态
+const recordList = ref<OtaTaskRecord[]>([]) // 记录列表数据
+const recordTotal = ref(0) // 记录总数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  taskId: undefined as number | undefined,
+  status: undefined as number | undefined
+}) // 查询参数
+const activeTab = ref('') // 当前激活的标签页
+
+/** 状态标签配置 */
+const statusTabs = computed(() => {
+  const tabs = [{ key: '', label: '全部设备' }]
+  Object.values(IoTOtaTaskRecordStatusEnum).forEach((status) => {
+    tabs.push({
+      key: status.value.toString(),
+      label: status.label
+    })
+  })
+  return tabs
+})
+
+/** 获取任务详情 */
+const getTaskInfo = async () => {
+  if (!taskId.value) {
+    return
+  }
+  taskLoading.value = true
+  try {
+    task.value = await IoTOtaTaskApi.getOtaTask(taskId.value)
+  } finally {
+    taskLoading.value = false
+  }
+}
+
+/** 获取统计数据 */
+const getStatistics = async () => {
+  if (!taskId.value) {
+    return
+  }
+  taskStatisticsLoading.value = true
+  try {
+    taskStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
+      undefined,
+      taskId.value
+    )
+  } finally {
+    taskStatisticsLoading.value = false
+  }
+}
+
+/** 获取升级记录列表 */
+const getRecordList = async () => {
+  if (!taskId.value) {
+    return
+  }
+  recordLoading.value = true
+  try {
+    queryParams.taskId = taskId.value
+    const data = await IoTOtaTaskRecordApi.getOtaTaskRecordPage(queryParams)
+    recordList.value = data.list || []
+    recordTotal.value = data.total || 0
+  } finally {
+    recordLoading.value = false
+  }
+}
+
+/** 切换标签 */
+const handleTabClick = (tab: TabsPaneContext) => {
+  const tabKey = tab.paneName as string
+  activeTab.value = tabKey
+  queryParams.pageNo = 1
+  queryParams.status = activeTab.value === '' ? undefined : parseInt(tabKey)
+  getRecordList()
+}
+
+/** 取消升级 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const handleCancelUpgrade = async (record: OtaTaskRecord) => {
+  try {
+    await message.confirm('确认要取消该设备的升级任务吗?')
+    await IoTOtaTaskRecordApi.cancelOtaTaskRecord(record.id!)
+    message.success('取消成功')
+    // 刷新数据
+    await getRecordList()
+    await getStatistics()
+    await getTaskInfo()
+    // 通知父组件刷新数据
+    emit('success')
+  } catch (error) {
+    console.error('取消升级失败', error)
+  }
+}
+
+/** 打开弹窗 */
+const open = (id: number) => {
+  taskId.value = id
+  dialogVisible.value = true
+  // 重置数据
+  activeTab.value = ''
+  queryParams.pageNo = 1
+  queryParams.status = undefined
+
+  // 加载数据
+  getTaskInfo()
+  getStatistics()
+  getRecordList()
+}
+
+/** 暴露方法 */
+defineExpose({ open })
+</script>

+ 132 - 0
src/views/iot/ota/task/OtaTaskForm.vue

@@ -0,0 +1,132 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="新增升级任务" width="800px" append-to-body>
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="任务名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入任务名称" />
+      </el-form-item>
+      <el-form-item label="任务描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          :rows="3"
+          placeholder="请输入任务描述"
+        />
+      </el-form-item>
+      <el-form-item label="升级范围" prop="deviceScope">
+        <el-select v-model="formData.deviceScope" placeholder="请选择升级范围" class="w-full">
+          <el-option
+            v-for="item in Object.values(IoTOtaTaskDeviceScopeEnum)"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="选择设备"
+        prop="deviceIds"
+        v-if="formData.deviceScope === IoTOtaTaskDeviceScopeEnum.SELECT.value"
+      >
+        <el-select
+          v-model="formData.deviceIds"
+          multiple
+          placeholder="请选择设备"
+          class="w-full"
+          filterable
+          reserve-keyword
+        >
+          <el-option
+            v-for="device in devices"
+            :key="device.id"
+            :label="
+              device.nickname ? `${device.deviceName} (${device.nickname})` : device.deviceName
+            "
+            :value="device.id"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { IoTOtaTaskDeviceScopeEnum } from '@/views/iot/utils/constants'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+
+/** IoT OTA 升级任务表单 */
+defineOptions({ name: 'OtaTaskForm' })
+
+const props = defineProps<{
+  firmwareId: number
+  productId: number
+}>()
+
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:修改时的数据加载
+const formData = ref<OtaTask>({
+  name: '',
+  deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
+  firmwareId: props.firmwareId,
+  description: '',
+  deviceIds: []
+})
+const formRef = ref() // 表单 Ref
+const formRules = {
+  name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
+  deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' }],
+  deviceIds: [{ required: true, message: '请至少选择一个设备', trigger: 'change' }]
+}
+const devices = ref<DeviceVO[]>([]) // 设备选择相关
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  resetForm()
+  // 加载设备列表
+  devices.value = (await DeviceApi.getDeviceListByProductId(props.productId)) || []
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    await IoTOtaTaskApi.createOtaTask(formData.value)
+    message.success('创建成功')
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    name: '',
+    deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
+    firmwareId: props.firmwareId,
+    description: '',
+    deviceIds: []
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 187 - 0
src/views/iot/ota/task/OtaTaskList.vue

@@ -0,0 +1,187 @@
+<template>
+  <ContentWrap title="升级任务管理" class="mb-20px">
+    <!-- 搜索栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+      @submit.prevent
+    >
+      <el-form-item>
+        <el-button type="primary" @click="openTaskForm" v-hasPermi="['iot:ota-task:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+      <el-form-item class="float-right">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入任务名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+
+    <!-- 任务列表 -->
+    <el-table
+      v-loading="taskLoading"
+      :data="taskList"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      class="mt-15px"
+    >
+      <el-table-column label="任务编号" align="center" prop="id" width="80" />
+      <el-table-column label="任务名称" align="center" prop="name" />
+      <el-table-column label="升级范围" align="center" prop="deviceScope">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE" :value="scope.row.deviceScope" />
+        </template>
+      </el-table-column>
+      <el-table-column label="升级进度" align="center">
+        <template #default="scope">
+          {{ scope.row.deviceSuccessCount }}/{{ scope.row.deviceTotalCount }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="任务描述" align="center" prop="description" show-overflow-tooltip />
+      <el-table-column label="任务状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="120">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleTaskDetail(scope.row.id)"> 详情 </el-button>
+          <el-button
+            v-if="scope.row.status === IoTOtaTaskStatusEnum.IN_PROGRESS.value"
+            link
+            type="danger"
+            @click="handleCancelTask(scope.row.id)"
+            v-hasPermi="['iot:ota-task:cancel']"
+          >
+            取消
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="taskTotal"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getTaskList"
+    />
+
+    <!-- 新增任务弹窗 -->
+    <OtaTaskForm
+      ref="taskFormRef"
+      :firmware-id="firmwareId"
+      :product-id="productId"
+      @success="handleTaskCreateSuccess"
+    />
+
+    <!-- 任务详情弹窗 -->
+    <OtaTaskDetail ref="taskDetailRef" @success="refresh" />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { DICT_TYPE } from '@/utils/dict'
+import { IoTOtaTaskStatusEnum } from '@/views/iot/utils/constants'
+import OtaTaskForm from './OtaTaskForm.vue'
+import OtaTaskDetail from './OtaTaskDetail.vue'
+
+/** IoT OTA 任务列表 */
+defineOptions({ name: 'OtaTaskList' })
+
+const props = defineProps<{
+  firmwareId: number
+  productId: number
+}>()
+
+const message = useMessage() // 消息弹窗
+
+// 任务列表
+const taskLoading = ref(false)
+const taskList = ref<OtaTask[]>([])
+const taskTotal = ref(0)
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  firmwareId: props.firmwareId
+})
+const queryFormRef = ref() // 查询表单引用
+const taskFormRef = ref() // 任务表单引用
+const taskDetailRef = ref() // 任务详情引用
+
+/** 获取任务列表 */
+const getTaskList = async () => {
+  taskLoading.value = true
+  try {
+    const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams)
+    taskList.value = data.list
+    taskTotal.value = data.total
+  } finally {
+    taskLoading.value = false
+  }
+}
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getTaskList()
+}
+
+/** 打开任务表单 */
+const openTaskForm = () => {
+  taskFormRef.value?.open()
+}
+
+/** 处理任务创建成功 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const handleTaskCreateSuccess = () => {
+  getTaskList()
+  emit('success')
+}
+
+/** 查看任务详情 */
+const handleTaskDetail = (id: number) => {
+  taskDetailRef.value?.open(id)
+}
+
+/** 取消任务 */
+const handleCancelTask = async (id: number) => {
+  try {
+    await message.confirm('确认要取消该升级任务吗?')
+    await IoTOtaTaskApi.cancelOtaTask(id)
+    message.success('取消成功')
+    // 刷新数据
+    await refresh()
+  } catch (error) {
+    console.error('取消任务失败', error)
+  }
+}
+
+/** 刷新数据 */
+const refresh = async () => {
+  await getTaskList()
+  emit('success')
+}
+
+/** 初始化 */
+onMounted(() => {
+  getTaskList()
+})
+</script>

+ 0 - 106
src/views/iot/plugin/PluginConfigForm.vue

@@ -1,106 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="插件名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入插件名称" />
-      </el-form-item>
-      <el-form-item label="部署方式" prop="deployType">
-        <el-select v-model="formData.deployType" placeholder="请选择部署方式">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { PluginConfigApi, PluginConfigVO } from '@/api/iot/plugin'
-
-/** IoT 插件配置 表单 */
-defineOptions({ name: 'PluginConfigForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  name: undefined,
-  deployType: undefined
-})
-const formRules = reactive({
-  name: [{ required: true, message: '插件名称不能为空', trigger: 'blur' }],
-  deployType: [{ required: true, message: '部署方式不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await PluginConfigApi.getPluginConfig(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as PluginConfigVO
-    if (formType.value === 'create') {
-      await PluginConfigApi.createPluginConfig(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await PluginConfigApi.updatePluginConfig(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: undefined,
-    deployType: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 99
src/views/iot/plugin/detail/PluginImportForm.vue

@@ -1,99 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="插件导入" width="400">
-    <el-upload
-      ref="uploadRef"
-      v-model:file-list="fileList"
-      :action="importUrl + '?id=' + props.id"
-      :auto-upload="false"
-      :disabled="formLoading"
-      :headers="uploadHeaders"
-      :limit="1"
-      :on-error="submitFormError"
-      :on-exceed="handleExceed"
-      :on-success="submitFormSuccess"
-      accept=".jar"
-      drag
-    >
-      <Icon icon="ep:upload" />
-      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
-    </el-upload>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { getAccessToken, getTenantId } from '@/utils/auth'
-
-defineOptions({ name: 'PluginImportForm' })
-
-const props = defineProps<{ id: number }>() // 接收 id 作为 props
-
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const uploadRef = ref()
-const importUrl =
-  import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/iot/plugin-config/upload-file'
-const uploadHeaders = ref() // 上传 Header 头
-const fileList = ref([]) // 文件列表
-
-/** 打开弹窗 */
-const open = () => {
-  dialogVisible.value = true
-  fileList.value = []
-  resetForm()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const submitForm = async () => {
-  if (fileList.value.length == 0) {
-    message.error('请上传文件')
-    return
-  }
-  // 提交请求
-  uploadHeaders.value = {
-    Authorization: 'Bearer ' + getAccessToken(),
-    'tenant-id': getTenantId()
-  }
-  formLoading.value = true
-  uploadRef.value!.submit()
-}
-
-/** 文件上传成功 */
-const emits = defineEmits(['success'])
-const submitFormSuccess = (response: any) => {
-  if (response.code !== 0) {
-    message.error(response.msg)
-    formLoading.value = false
-    return
-  }
-  message.alert('上传成功')
-  formLoading.value = false
-  dialogVisible.value = false
-  // 发送操作成功的事件
-  emits('success')
-}
-
-/** 上传错误提示 */
-const submitFormError = (): void => {
-  message.error('上传失败,请您重新上传!')
-  formLoading.value = false
-}
-
-/** 重置表单 */
-const resetForm = async (): Promise<void> => {
-  // 重置上传状态和文件
-  formLoading.value = false
-  await nextTick()
-  uploadRef.value?.clearFiles()
-}
-
-/** 文件数超出提示 */
-const handleExceed = (): void => {
-  message.error('最多只能上传一个文件!')
-}
-</script>

+ 0 - 120
src/views/iot/plugin/detail/index.vue

@@ -1,120 +0,0 @@
-<template>
-  <div>
-    <div class="flex items-start justify-between">
-      <div>
-        <el-col>
-          <el-row>
-            <span class="text-xl font-bold">插件配置</span>
-          </el-row>
-        </el-col>
-      </div>
-    </div>
-    <ContentWrap class="mt-10px">
-      <el-descriptions :column="2" direction="horizontal">
-        <el-descriptions-item label="插件名称">
-          {{ pluginConfig.name }}
-        </el-descriptions-item>
-        <el-descriptions-item label="插件标识">
-          {{ pluginConfig.pluginKey }}
-        </el-descriptions-item>
-        <el-descriptions-item label="版本号">
-          {{ pluginConfig.version }}
-        </el-descriptions-item>
-        <el-descriptions-item label="状态">
-          <el-switch
-            v-model="pluginConfig.status"
-            :active-value="1"
-            :inactive-value="0"
-            :disabled="pluginConfig.id <= 0"
-            @change="handleStatusChange"
-          />
-        </el-descriptions-item>
-        <el-descriptions-item label="插件描述">
-          {{ pluginConfig.description }}
-        </el-descriptions-item>
-      </el-descriptions>
-    </ContentWrap>
-    <!-- TODO @haohao:如果是独立部署,也是通过上传插件包哇? -->
-    <ContentWrap class="mt-10px">
-      <el-button type="warning" plain @click="handleImport" v-hasPermi="['system:user:import']">
-        <Icon icon="ep:upload" /> 上传插件包
-      </el-button>
-    </ContentWrap>
-  </div>
-  <!-- TODO @haohao:待完成:配置管理 -->
-  <!-- TODO @haohao:待完成:script 管理;可以最后搞 -->
-  <!-- TODO @haohao:插件实例的前端展示:底部要不要加个分页,展示运行中的实力?默认勾选,只展示 state 为在线的 -->
-
-  <!-- 插件导入对话框 -->
-  <PluginImportForm
-    ref="importFormRef"
-    :id="pluginConfig.id"
-    @success="getPluginConfig(pluginConfig.id)"
-  />
-</template>
-
-<script lang="ts" setup>
-import { PluginConfigApi, PluginConfigVO } from '@/api/iot/plugin'
-import { useRoute } from 'vue-router'
-import { onMounted, ref } from 'vue'
-import PluginImportForm from './PluginImportForm.vue'
-
-const message = useMessage()
-const route = useRoute()
-const pluginConfig = ref<PluginConfigVO>({
-  id: 0,
-  pluginKey: '',
-  name: '',
-  description: '',
-  version: '',
-  status: 0,
-  deployType: 0,
-  fileName: '',
-  type: 0,
-  protocol: '',
-  configSchema: '',
-  config: '',
-  script: ''
-})
-
-/** 获取插件配置 */
-const getPluginConfig = async (id: number) => {
-  pluginConfig.value = await PluginConfigApi.getPluginConfig(id)
-}
-
-/** 处理状态变更 */
-const handleStatusChange = async (status: number) => {
-  if (pluginConfig.value.id <= 0) {
-    return
-  }
-  try {
-    // 修改状态的二次确认
-    const text = status === 1 ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '"插件吗?')
-    await PluginConfigApi.updatePluginStatus({
-      id: pluginConfig.value.id,
-      status
-    })
-    message.success('更新状态成功')
-    // 获取配置
-    await getPluginConfig(pluginConfig.value.id)
-  } catch (error) {
-    pluginConfig.value.status = status === 1 ? 0 : 1
-    message.error('更新状态失败')
-  }
-}
-
-/** 插件导入 */
-const importFormRef = ref()
-const handleImport = () => {
-  importFormRef.value.open()
-}
-
-/** 初始化插件配置 */
-onMounted(() => {
-  const id = Number(route.params.id)
-  if (id) {
-    getPluginConfig(id)
-  }
-})
-</script>

+ 0 - 329
src/views/iot/plugin/index.vue

@@ -1,329 +0,0 @@
-<!-- TODO @haohao:搞到 config 目录,会不会更好哈 -->
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="插件名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入插件名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-select
-          v-model="queryParams.status"
-          placeholder="请选择状态"
-          clearable
-          @change="handleQuery"
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PLUGIN_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item class="float-right !mr-0 !mb-0">
-        <el-button-group>
-          <el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
-            <Icon icon="ep:grid" />
-          </el-button>
-          <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
-            <Icon icon="ep:list" />
-          </el-button>
-        </el-button-group>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['iot:plugin-config:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <template v-if="viewMode === 'list'">
-      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column label="插件名称" align="center" prop="name" />
-        <el-table-column label="插件标识" align="center" prop="pluginKey" />
-        <el-table-column label="jar 包" align="center" prop="fileName" />
-        <el-table-column label="版本号" align="center" prop="version" />
-        <el-table-column label="部署方式" align="center" prop="deployType">
-          <template #default="scope">
-            <dict-tag :type="DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE" :value="scope.row.deployType" />
-          </template>
-        </el-table-column>
-        <el-table-column label="状态" align="center" prop="status">
-          <template #default="scope">
-            <el-switch
-              v-model="scope.row.status"
-              :active-value="1"
-              :inactive-value="0"
-              @change="handleStatusChange(scope.row.id, Number($event))"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column
-          label="创建时间"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-          width="180px"
-        />
-        <el-table-column label="操作" align="center" min-width="120px">
-          <template #default="scope">
-            <el-button
-              link
-              type="primary"
-              @click="openDetail(scope.row.id)"
-              v-hasPermi="['iot:product:query']"
-            >
-              查看
-            </el-button>
-            <el-button
-              link
-              type="primary"
-              @click="openForm('update', scope.row.id)"
-              v-hasPermi="['iot:plugin-config:update']"
-            >
-              编辑
-            </el-button>
-            <el-button
-              link
-              type="danger"
-              @click="handleDelete(scope.row.id)"
-              v-hasPermi="['iot:plugin-config:delete']"
-            >
-              删除
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </template>
-    <template v-if="viewMode === 'card'">
-      <el-row :gutter="16">
-        <el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
-          <el-card
-            class="h-full transition-colors relative overflow-hidden"
-            :body-style="{ padding: '0' }"
-          >
-            <div class="p-4 relative">
-              <!-- 标题区域 -->
-              <div class="flex items-center mb-3">
-                <div class="mr-2.5 flex items-center">
-                  <el-image :src="defaultIconUrl" class="w-[18px] h-[18px]" />
-                </div>
-                <div class="text-[16px] font-600 flex-1">{{ item.name }}</div>
-                <!-- 添加插件状态标签 -->
-                <div class="inline-flex items-center">
-                  <div
-                    class="w-1 h-1 rounded-full mr-1.5"
-                    :class="
-                      item.status === 1
-                        ? 'bg-[var(--el-color-success)]'
-                        : 'bg-[var(--el-color-danger)]'
-                    "
-                  >
-                  </div>
-                  <el-text
-                    class="!text-xs font-bold"
-                    :type="item.status === 1 ? 'success' : 'danger'"
-                  >
-                    {{ item.status === 1 ? '开启' : '禁用' }}
-                  </el-text>
-                </div>
-              </div>
-
-              <!-- 信息区域 -->
-              <div class="flex items-center text-[14px]">
-                <div class="flex-1">
-                  <div class="mb-2.5 last:mb-0">
-                    <span class="text-[#717c8e] mr-2.5">插件标识</span>
-                    <span class="text-[#0b1d30] whitespace-normal break-all">
-                      {{ item.pluginKey }}
-                    </span>
-                  </div>
-                  <div class="mb-2.5 last:mb-0">
-                    <span class="text-[#717c8e] mr-2.5">jar 包</span>
-                    <span class="text-[#0b1d30]">{{ item.fileName }}</span>
-                  </div>
-                  <div class="mb-2.5 last:mb-0">
-                    <span class="text-[#717c8e] mr-2.5">版本号</span>
-                    <span class="text-[#0b1d30]">{{ item.version }}</span>
-                  </div>
-                  <div class="mb-2.5 last:mb-0">
-                    <span class="text-[#717c8e] mr-2.5">部署方式</span>
-                    <dict-tag :type="DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE" :value="item.deployType" />
-                  </div>
-                </div>
-              </div>
-
-              <!-- 分隔线 -->
-              <el-divider class="!my-3" />
-
-              <!-- 按钮 -->
-              <div class="flex items-center px-0">
-                <el-button
-                  class="flex-1 !px-2 !h-[32px] text-[13px]"
-                  type="primary"
-                  plain
-                  @click="openForm('update', item.id)"
-                  v-hasPermi="['iot:plugin-config:update']"
-                >
-                  <Icon icon="ep:edit-pen" class="mr-1" />
-                  编辑
-                </el-button>
-                <el-button
-                  class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
-                  type="warning"
-                  plain
-                  @click="openDetail(item.id)"
-                >
-                  <Icon icon="ep:view" class="mr-1" />
-                  详情
-                </el-button>
-                <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
-                <el-button
-                  class="!px-2 !h-[32px] text-[13px]"
-                  type="danger"
-                  plain
-                  @click="handleDelete(item.id)"
-                  v-hasPermi="['iot:device:delete']"
-                >
-                  <Icon icon="ep:delete" />
-                </el-button>
-              </div>
-            </div>
-          </el-card>
-        </el-col>
-      </el-row>
-    </template>
-
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <PluginConfigForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import { PluginConfigApi, PluginConfigVO } from '@/api/iot/plugin'
-import PluginConfigForm from './PluginConfigForm.vue'
-
-/** IoT 插件配置 列表 */
-defineOptions({ name: 'IoTPlugin' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
-const loading = ref(true) // 列表的加载中
-const list = ref<PluginConfigVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  name: undefined,
-  status: undefined
-})
-const queryFormRef = ref() // 搜索的表单
-const defaultIconUrl = ref('/src/assets/svgs/iot/card-fill.svg') // 默认插件图标
-const viewMode = ref<'card' | 'list'>('card') // 视图模式状态
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await PluginConfigApi.getPluginConfigPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-
-/** 打开详情 */
-const { push } = useRouter()
-const openDetail = (id: number) => {
-  push({ name: 'IoTPluginDetail', params: { id } })
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await PluginConfigApi.deletePluginConfig(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 处理状态变更 */
-const handleStatusChange = async (id: number, status: number) => {
-  try {
-    // 修改状态的二次确认
-    const text = status === 1 ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '"插件吗?')
-    await PluginConfigApi.updatePluginStatus({
-      id: id,
-      status
-    })
-    message.success('更新状态成功')
-    getList()
-  } catch (error) {
-    message.error('更新状态失败')
-  }
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>

+ 0 - 1
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 () => {

+ 15 - 50
src/views/iot/product/product/ProductForm.vue

@@ -45,7 +45,7 @@
         </el-radio-group>
       </el-form-item>
       <el-form-item
-        v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(formData.deviceType)"
+        v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(formData.deviceType!)"
         label="联网方式"
         prop="netType"
       >
@@ -62,28 +62,10 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item
-        v-if="formData.deviceType === DeviceTypeEnum.GATEWAY_SUB"
-        label="接入网关协议"
-        prop="protocolType"
-      >
-        <el-select
-          v-model="formData.protocolType"
-          placeholder="请选择接入网关协议"
-          :disabled="formType === 'update'"
-        >
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="数据格式" prop="dataFormat">
-        <el-radio-group v-model="formData.dataFormat" :disabled="formType === 'update'">
+      <el-form-item label="定位类型" prop="locationType">
+        <el-radio-group v-model="formData.locationType" :disabled="formType === 'update'">
           <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_FORMAT)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_LOCATION_TYPE)"
             :key="dict.value"
             :label="dict.value"
           >
@@ -91,10 +73,10 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="数据校验级别" prop="validateType">
-        <el-radio-group v-model="formData.validateType" :disabled="formType === 'update'">
+      <el-form-item label="数据格式" prop="codecType">
+        <el-radio-group v-model="formData.codecType" :disabled="formType === 'update'">
           <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE)"
+            v-for="dict in getStrDictOptions(DICT_TYPE.IOT_CODEC_TYPE)"
             :key="dict.value"
             :label="dict.value"
           >
@@ -124,14 +106,8 @@
 </template>
 
 <script setup lang="ts">
-import {
-  ValidateTypeEnum,
-  ProductApi,
-  ProductVO,
-  DataFormatEnum,
-  DeviceTypeEnum
-} from '@/api/iot/product/product'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { ProductApi, ProductVO, CodecTypeEnum, DeviceTypeEnum } from '@/api/iot/product/product'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
 import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category'
 import { UploadImg } from '@/components/UploadFile'
 import { generateRandomStr } from '@/utils'
@@ -154,17 +130,16 @@ const formData = ref({
   picUrl: undefined,
   description: undefined,
   deviceType: undefined,
+  locationType: undefined,
   netType: undefined,
-  protocolType: undefined,
-  protocolId: undefined,
-  dataFormat: DataFormatEnum.JSON,
-  validateType: ValidateTypeEnum.WEAK
+  codecType: CodecTypeEnum.ALINK
 })
 const formRules = reactive({
   productKey: [{ required: true, message: 'ProductKey 不能为空', trigger: 'blur' }],
   name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
   categoryId: [{ required: true, message: '产品分类不能为空', trigger: 'change' }],
   deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
+  locationType: [{ required: true, message: '定位类型不能为空', trigger: 'change' }],
   netType: [
     {
       required: true,
@@ -172,15 +147,7 @@ const formRules = reactive({
       trigger: 'change'
     }
   ],
-  protocolType: [
-    {
-      required: true,
-      message: '接入网关协议不能为空',
-      trigger: 'change'
-    }
-  ],
-  dataFormat: [{ required: true, message: '数据格式不能为空', trigger: 'change' }],
-  validateType: [{ required: true, message: '数据校验级别不能为空', trigger: 'change' }]
+  codecType: [{ required: true, message: '数据格式不能为空', trigger: 'change' }]
 })
 const formRef = ref()
 const categoryList = ref<ProductCategoryVO[]>([]) // 产品分类列表
@@ -239,11 +206,9 @@ const resetForm = () => {
     picUrl: undefined,
     description: undefined,
     deviceType: undefined,
+    locationType: undefined,
     netType: undefined,
-    protocolType: undefined,
-    protocolId: undefined,
-    dataFormat: DataFormatEnum.JSON,
-    validateType: ValidateTypeEnum.WEAK
+    codecType: CodecTypeEnum.ALINK
   }
   formRef.value?.resetFields()
 }

+ 220 - 0
src/views/iot/product/product/components/ProductTableSelect.vue

@@ -0,0 +1,220 @@
+<!-- IoT 产品选择,使用弹窗展示 -->
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
+    <ContentWrap>
+      <!-- 搜索工作栏 -->
+      <el-form
+        ref="queryFormRef"
+        :inline="true"
+        :model="queryParams"
+        class="-mb-15px"
+        label-width="68px"
+      >
+        <el-form-item label="产品名称" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            class="!w-240px"
+            clearable
+            placeholder="请输入产品名称"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="ProductKey" prop="productKey">
+          <el-input
+            v-model="queryParams.productKey"
+            class="!w-240px"
+            clearable
+            placeholder="请输入产品标识"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon class="mr-5px" icon="ep:search" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon class="mr-5px" icon="ep:refresh" />
+            重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="list"
+        :show-overflow-tooltip="true"
+        :stripe="true"
+        @row-click="handleRowClick"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column v-if="multiple" type="selection" width="55" />
+        <el-table-column v-else width="55">
+          <template #default="scope">
+            <el-radio
+              v-model="selectedId"
+              :value="scope.row.id"
+              @change="() => handleRadioChange(scope.row)"
+            >
+              &nbsp;
+            </el-radio>
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="名称" prop="name" />
+        <el-table-column align="center" label="ProductKey" prop="productKey" />
+        <el-table-column align="center" label="品类" prop="categoryName" />
+        <el-table-column align="center" label="设备类型" prop="deviceType">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="产品图标" prop="icon">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.icon"
+              :preview-src-list="[scope.row.icon]"
+              :src="scope.row.icon"
+              class="w-40px h-40px"
+            />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="产品图片" prop="picture">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.picUrl"
+              :preview-src-list="[scope.row.picture]"
+              :src="scope.row.picUrl"
+              class="w-40px h-40px"
+            />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="创建时间"
+          prop="createTime"
+          width="180px"
+        />
+      </el-table>
+
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
+        @pagination="getList"
+      />
+    </ContentWrap>
+
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+
+defineOptions({ name: 'IoTProductTableSelect' })
+
+const props = defineProps({
+  multiple: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const message = useMessage()
+const dialogVisible = ref(false)
+const dialogTitle = ref('产品选择器')
+const formLoading = ref(false)
+const loading = ref(true) // 列表的加载中
+const list = ref<ProductVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const selectedProducts = ref<ProductVO[]>([]) // 选中的产品列表
+const selectedId = ref<number>() // 单选模式下选中的ID
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  productKey: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProductApi.getProductPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  // 重置选择状态
+  selectedProducts.value = []
+  selectedId.value = undefined
+  await getList()
+}
+defineExpose({ open })
+
+/** 处理行点击事件 */
+const tableRef = ref()
+const handleRowClick = (row: ProductVO) => {
+  if (props.multiple) {
+    tableRef.value?.toggleRowSelection(row)
+  } else {
+    selectedId.value = row.id
+    selectedProducts.value = [row]
+  }
+}
+
+/** 处理单选变更事件 */
+const handleRadioChange = (row: ProductVO) => {
+  selectedProducts.value = [row]
+}
+
+/** 处理选择变更事件 */
+const handleSelectionChange = (selection: ProductVO[]) => {
+  if (props.multiple) {
+    selectedProducts.value = selection
+  }
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  if (selectedProducts.value.length === 0) {
+    message.warning(props.multiple ? '请至少选择一个产品' : '请选择一个产品')
+    return
+  }
+  emit('success', props.multiple ? selectedProducts.value : selectedProducts.value[0])
+  dialogVisible.value = false
+}
+</script>

+ 4 - 6
src/views/iot/product/product/detail/ProductDetailsHeader.vue

@@ -13,7 +13,7 @@
         <el-button
           @click="openForm('update', product.id)"
           v-hasPermi="['iot:product:update']"
-          v-if="product.status === 0"
+          :disabled="product.status === 1"
         >
           编辑
         </el-button>
@@ -37,15 +37,13 @@
     </div>
   </div>
   <ContentWrap class="mt-10px">
-    <el-descriptions :column="5" direction="horizontal">
+    <el-descriptions :column="1" direction="horizontal">
       <el-descriptions-item label="ProductKey">
         {{ product.productKey }}
         <el-button @click="copyToClipboard(product.productKey)">复制</el-button>
       </el-descriptions-item>
-    </el-descriptions>
-    <el-descriptions :column="5" direction="horizontal">
-      <el-descriptions-item label="设备数">
-        {{ product.deviceCount ?? '加载中...' }}
+      <el-descriptions-item label="设备总数">
+        <span class="ml-20px mr-10px">{{ product.deviceCount ?? '加载中...' }}</span>
         <el-button @click="goToDeviceList(product.id)">前往管理</el-button>
       </el-descriptions-item>
     </el-descriptions>

+ 5 - 11
src/views/iot/product/product/detail/ProductDetailsInfo.vue

@@ -1,19 +1,19 @@
 <template>
   <ContentWrap>
-    <el-descriptions :column="3" title="产品信息">
+    <el-descriptions :column="3" title="产品信息" border>
       <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
       <el-descriptions-item label="所属分类">{{ product.categoryName }}</el-descriptions-item>
       <el-descriptions-item label="设备类型">
         <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
       </el-descriptions-item>
+      <el-descriptions-item label="定位类型">
+        <dict-tag :type="DICT_TYPE.IOT_LOCATION_TYPE" :value="product.locationType" />
+      </el-descriptions-item>
       <el-descriptions-item label="创建时间">
         {{ formatDate(product.createTime) }}
       </el-descriptions-item>
       <el-descriptions-item label="数据格式">
-        <dict-tag :type="DICT_TYPE.IOT_DATA_FORMAT" :value="product.dataFormat" />
-      </el-descriptions-item>
-      <el-descriptions-item label="数据校验级别">
-        <dict-tag :type="DICT_TYPE.IOT_VALIDATE_TYPE" :value="product.validateType" />
+        <dict-tag :type="DICT_TYPE.IOT_CODEC_TYPE" :value="product.codecType" />
       </el-descriptions-item>
       <el-descriptions-item label="产品状态">
         <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
@@ -24,12 +24,6 @@
       >
         <dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
       </el-descriptions-item>
-      <el-descriptions-item
-        label="接入网关协议"
-        v-if="product.deviceType === DeviceTypeEnum.GATEWAY_SUB"
-      >
-        <dict-tag :type="DICT_TYPE.IOT_PROTOCOL_TYPE" :value="product.protocolType" />
-      </el-descriptions-item>
       <el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
     </el-descriptions>
   </ContentWrap>

+ 0 - 247
src/views/iot/product/product/detail/ProductTopic.vue

@@ -1,247 +0,0 @@
-<template>
-  <ContentWrap>
-    <el-tabs>
-      <el-tab-pane label="基础通信 Topic">
-        <Table
-          :columns="basicColumn"
-          :data="basicData"
-          :span-method="createSpanMethod(basicData)"
-          align="left"
-          headerAlign="left"
-          border="true"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="物模型通信 Topic">
-        <Table
-          :columns="functionColumn"
-          :data="functionData"
-          :span-method="createSpanMethod(functionData)"
-          align="left"
-          headerAlign="left"
-          border="true"
-        />
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-</template>
-<script setup lang="ts">
-import { ProductVO } from '@/api/iot/product/product'
-
-const props = defineProps<{ product: ProductVO }>()
-
-// TODO 芋艿:不确定未来会不会改,所以先写死
-
-// 基础通信 Topic 列
-const basicColumn = reactive([
-  { label: '功能', field: 'function', width: 150 },
-  { label: 'Topic 类', field: 'topicClass', width: 800 },
-  { label: '操作权限', field: 'operationPermission', width: 100 },
-  { label: '描述', field: 'description' }
-])
-
-// 基础通信 Topic 数据
-const basicData = computed(() => {
-  if (!props.product || !props.product.productKey) return []
-  return [
-    {
-      function: 'OTA 升级',
-      topicClass: `/ota/device/inform/${props.product.productKey}/\${deviceName}`,
-      operationPermission: '发布',
-      description: '设备上报固件升级信息'
-    },
-    {
-      function: 'OTA 升级',
-      topicClass: `/ota/device/upgrade/${props.product.productKey}/\${deviceName}`,
-      operationPermission: '订阅',
-      description: '固件升级信息下行'
-    },
-    {
-      function: 'OTA 升级',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
-      operationPermission: '发布',
-      description: '设备上报固件升级进度'
-    },
-    {
-      function: 'OTA 升级',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
-      operationPermission: '发布',
-      description: '设备主动拉取固件升级信息'
-    },
-    {
-      function: '设备标签',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update`,
-      operationPermission: '发布',
-      description: '设备上报标签数据'
-    },
-    {
-      function: '设备标签',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update_reply`,
-      operationPermission: '订阅',
-      description: '云端响应标签上报'
-    },
-    {
-      function: '设备标签',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete`,
-      operationPermission: '订阅',
-      description: '设备删除标签信息'
-    },
-    {
-      function: '设备标签',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete_reply`,
-      operationPermission: '订阅',
-      description: '云端响应标签删除'
-    },
-    {
-      function: '时钟同步',
-      topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/request`,
-      operationPermission: '发布',
-      description: 'NTP 时钟同步请求'
-    },
-    {
-      function: '时钟同步',
-      topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/response`,
-      operationPermission: '订阅',
-      description: 'NTP 时钟同步响应'
-    },
-    {
-      function: '设备影子',
-      topicClass: `/shadow/update/${props.product.productKey}/\${deviceName}`,
-      operationPermission: '发布',
-      description: '设备影子发布'
-    },
-    {
-      function: '设备影子',
-      topicClass: `/shadow/get/${props.product.productKey}/\${deviceName}`,
-      operationPermission: '订阅',
-      description: '设备接收影子变更'
-    },
-    {
-      function: '配置更新',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/push`,
-      operationPermission: '订阅',
-      description: '云端主动下推配置信息'
-    },
-    {
-      function: '配置更新',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get`,
-      operationPermission: '发布',
-      description: '设备端查询配置信息'
-    },
-    {
-      function: '配置更新',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get_reply`,
-      operationPermission: '订阅',
-      description: '云端响应配置信息'
-    },
-    {
-      function: '广播',
-      topicClass: `/broadcast/${props.product.productKey}/\${identifier}`,
-      operationPermission: '订阅',
-      description: '广播 Topic,identifier 为用户自定义字符串'
-    }
-  ]
-})
-
-// 物模型通信 Topic 列
-const functionColumn = reactive([
-  { label: '功能', field: 'function', width: 150 },
-  { label: 'Topic 类', field: 'topicClass', width: 800 },
-  { label: '操作权限', field: 'operationPermission', width: 100 },
-  { label: '描述', field: 'description' }
-])
-
-// 物模型通信 Topic 数据
-const functionData = computed(() => {
-  if (!props.product || !props.product.productKey) return []
-  return [
-    {
-      function: '属性上报',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post`,
-      operationPermission: '发布',
-      description: '设备属性上报'
-    },
-    {
-      function: '属性上报',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post_reply`,
-      operationPermission: '订阅',
-      description: '云端响应属性上报'
-    },
-    {
-      function: '属性设置',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/property/set`,
-      operationPermission: '订阅',
-      description: '设备属性设置'
-    },
-    {
-      function: '事件上报',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post`,
-      operationPermission: '发布',
-      description: '设备事件上报'
-    },
-    {
-      function: '事件上报',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post_reply`,
-      operationPermission: '订阅',
-      description: '云端响应事件上报'
-    },
-    {
-      function: '服务调用',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}`,
-      operationPermission: '订阅',
-      description: '设备服务调用'
-    },
-    {
-      function: '服务调用',
-      topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}_reply`,
-      operationPermission: '发布',
-      description: '设备端响应服务调用'
-    }
-  ]
-})
-
-// 通用的单元格合并方法生成器
-const createSpanMethod = (data: any[]) => {
-  // 预处理,计算每个功能的合并行数
-  const rowspanMap: Record<number, number> = {}
-  let currentFunction = ''
-  let startIndex = 0
-  let count = 0
-
-  data.forEach((item, index) => {
-    if (item.function !== currentFunction) {
-      if (count > 0) {
-        rowspanMap[startIndex] = count
-      }
-      currentFunction = item.function
-      startIndex = index
-      count = 1
-    } else {
-      count++
-    }
-  })
-
-  // 处理最后一组
-  if (count > 0) {
-    rowspanMap[startIndex] = count
-  }
-
-  // 返回 span 方法
-  return ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
-    if (columnIndex === 0) {
-      // 仅对“功能”列进行合并
-      const rowspan = rowspanMap[rowIndex] || 0
-      if (rowspan > 0) {
-        return {
-          rowspan,
-          colspan: 1
-        }
-      } else {
-        return {
-          rowspan: 0,
-          colspan: 0
-        }
-      }
-    }
-  }
-}
-</script>

+ 1 - 7
src/views/iot/product/product/detail/index.vue

@@ -5,14 +5,9 @@
       <el-tab-pane label="产品信息" name="info">
         <ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
       </el-tab-pane>
-      <el-tab-pane label="Topic 类列表" name="topic">
-        <ProductTopic v-if="activeTab === 'topic'" :product="product" />
-      </el-tab-pane>
-      <el-tab-pane label="功能定义" lazy name="thingModel">
+      <el-tab-pane label="物模型(功能定义)" lazy name="thingModel">
         <IoTProductThingModel ref="thingModelRef" />
       </el-tab-pane>
-      <el-tab-pane label="消息解析" name="message" />
-      <el-tab-pane label="服务端订阅" name="subscription" />
     </el-tabs>
   </el-col>
 </template>
@@ -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'

+ 20 - 0
src/views/iot/rule/data/index.vue

@@ -0,0 +1,20 @@
+<template>
+  <el-tabs v-model="activeTab" type="border-card">
+    <el-tab-pane label="规则" name="rule">
+      <RuleIndex />
+    </el-tab-pane>
+    <el-tab-pane label="目的" name="sink" lazy>
+      <SinkIndex />
+    </el-tab-pane>
+  </el-tabs>
+</template>
+
+<script setup lang="ts">
+import RuleIndex from './rule/index.vue'
+import SinkIndex from './sink/index.vue'
+
+/** IoT 数据流转 */
+defineOptions({ name: 'IoTDataRule' })
+
+const activeTab = ref('rule')
+</script>

+ 158 - 0
src/views/iot/rule/data/rule/DataRuleForm.vue

@@ -0,0 +1,158 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="870">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="规则名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入规则名称" />
+      </el-form-item>
+      <el-form-item label="规则描述" prop="description">
+        <el-input v-model="formData.description" height="150px" type="textarea" />
+      </el-form-item>
+      <el-form-item label="规则状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="数据目的" prop="sinkIds">
+        <el-select
+          v-model="formData.sinkIds"
+          placeholder="请选择数据目的"
+          multiple
+          clearable
+          class="w-1/1"
+        >
+          <el-option
+            v-for="sink in dataSinkList"
+            :key="sink.id"
+            :label="sink.name"
+            :value="sink.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="数据源" prop="sourceConfigs">
+        <SourceConfigForm ref="sourceConfigRef" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { DataRuleApi, DataRule } from '@/api/iot/rule/data/rule'
+import { DataSinkApi } from '@/api/iot/rule/data/sink'
+import { CommonStatusEnum } from '@/utils/constants'
+import SourceConfigForm from './components/SourceConfigForm.vue'
+
+/** IoT 数据流转规则的表单 */
+defineOptions({ name: 'DataRuleForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  status: CommonStatusEnum.ENABLE,
+  sourceConfigs: [],
+  sinkIds: []
+})
+const formRules = reactive({
+  name: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '规则状态不能为空', trigger: 'blur' }],
+  sourceConfigs: [{ required: true, message: '数据源配置数组不能为空', trigger: 'blur' }],
+  sinkIds: [{ required: true, message: '数据目的编号数组不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const dataSinkList = ref<any[]>([]) // 数据目的列表
+const sourceConfigRef = ref() // 数据源配置组件引用
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const data = await DataRuleApi.getDataRule(id)
+      formData.value = data
+      // 设置数据源配置
+      nextTick(() => {
+        sourceConfigRef.value?.setData(data.sourceConfigs || [])
+      })
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  // 加载数据目的列表
+  dataSinkList.value = await DataSinkApi.getDataSinkSimpleList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验数据源配置
+  await sourceConfigRef.value?.validate()
+  formData.value.sourceConfigs = sourceConfigRef.value?.getData() || []
+  // 校验表单
+  await formRef.value.validate()
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = { ...formData.value } as unknown as DataRule
+    if (formType.value === 'create') {
+      await DataRuleApi.createDataRule(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DataRuleApi.updateDataRule(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = async () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    status: CommonStatusEnum.ENABLE,
+    sourceConfigs: [],
+    sinkIds: []
+  }
+  formRef.value?.resetFields()
+  // 重置数据源配置
+  await nextTick()
+  sourceConfigRef.value?.setData([])
+}
+</script>

+ 262 - 0
src/views/iot/rule/data/rule/components/SourceConfigForm.vue

@@ -0,0 +1,262 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="产品" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+            <el-select
+              v-model="row.productId"
+              placeholder="请选择产品"
+              @change="handleProductChange(row, $index)"
+              clearable
+              filterable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="product in productList"
+                :key="product.id"
+                :label="product.name"
+                :value="product.id"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="设备" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.deviceId`" :rules="formRules.deviceId" class="mb-0px!">
+            <el-select
+              v-model="row.deviceId"
+              placeholder="请选择设备"
+              clearable
+              filterable
+              style="width: 100%"
+            >
+              <el-option label="全部设备" :value="0" />
+              <el-option
+                v-for="device in getFilteredDevices(row.productId)"
+                :key="device.id"
+                :label="device.deviceName"
+                :value="device.id"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="消息" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.method`" :rules="formRules.method" class="mb-0px!">
+            <el-select
+              v-model="row.method"
+              placeholder="请选择消息"
+              @change="handleMethodChange(row, $index)"
+              clearable
+              filterable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="method in upstreamMethods"
+                :key="method.method"
+                :label="method.name"
+                :value="method.method"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="标识符" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.identifier`" class="mb-0px!">
+            <el-select
+              v-if="shouldShowIdentifierSelect(row)"
+              v-model="row.identifier"
+              placeholder="请选择标识符"
+              clearable
+              filterable
+              style="width: 100%"
+              v-loading="row.identifierLoading"
+            >
+              <el-option
+                v-for="item in getThingModelOptions(row)"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link type="danger">—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-row justify="center" class="mt-3">
+      <el-button @click="handleAdd" type="primary" plain round>+ 添加数据源</el-button>
+    </el-row>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ProductApi } from '@/api/iot/product/product'
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
+
+const formData = ref<any[]>([])
+const productList = ref<any[]>([]) // 产品列表
+const deviceList = ref<any[]>([]) // 设备列表
+const thingModelCache = ref<Map<number, any[]>>(new Map()) // 缓存物模型数据,key 为 productId
+
+const formRules = reactive({
+  productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
+  deviceId: [{ required: true, message: '设备不能为空', trigger: 'change' }],
+  method: [{ required: true, message: '消息方法不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 获取上行消息方法列表
+const upstreamMethods = computed(() => {
+  return Object.values(IotDeviceMessageMethodEnum).filter((item) => item.upstream)
+})
+
+/** 根据产品 ID 过滤设备 */
+const getFilteredDevices = (productId: number) => {
+  if (!productId) return []
+  return deviceList.value.filter((device: any) => device.productId === productId)
+}
+
+/** 判断是否需要显示标识符选择器 */
+const shouldShowIdentifierSelect = (row: any) => {
+  return [
+    IotDeviceMessageMethodEnum.EVENT_POST.method,
+    IotDeviceMessageMethodEnum.PROPERTY_POST.method
+  ].includes(row.method)
+}
+
+/** 获取物模型选项 */
+const getThingModelOptions = (row: any) => {
+  if (!row.productId || !shouldShowIdentifierSelect(row)) {
+    return []
+  }
+  const thingModels: any[] = thingModelCache.value.get(row.productId) || []
+  let filteredModels: any[] = []
+  if (row.method === IotDeviceMessageMethodEnum.EVENT_POST.method) {
+    filteredModels = thingModels.filter((item: any) => item.type === IoTThingModelTypeEnum.EVENT)
+  } else if (row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method) {
+    filteredModels = thingModels.filter((item: any) => item.type === IoTThingModelTypeEnum.PROPERTY)
+  }
+  return filteredModels.map((item: any) => ({
+    label: `${item.name} (${item.identifier})`,
+    value: item.identifier
+  }))
+}
+
+/** 加载产品列表 */
+const loadProductList = async () => {
+  try {
+    productList.value = await ProductApi.getSimpleProductList()
+  } catch (error) {
+    console.error('加载产品列表失败:', error)
+  }
+}
+
+/** 加载设备列表 */
+const loadDeviceList = async () => {
+  try {
+    deviceList.value = await DeviceApi.getSimpleDeviceList()
+  } catch (error) {
+    console.error('加载设备列表失败:', error)
+  }
+}
+
+/** 加载物模型数据 */
+const loadThingModel = async (productId: number) => {
+  // 已缓存,无需重复加载
+  if (thingModelCache.value.has(productId)) {
+    return
+  }
+  try {
+    const thingModels = await ThingModelApi.getThingModelList({ productId })
+    thingModelCache.value.set(productId, thingModels)
+  } catch (error) {
+    console.error('加载物模型失败:', error)
+  }
+}
+
+/** 产品变化时处理 */
+const handleProductChange = async (row: any, _index: number) => {
+  row.deviceId = 0
+  row.method = undefined
+  row.identifier = undefined
+  row.identifierLoading = false
+}
+
+/** 消息方法变化时处理 */
+const handleMethodChange = async (row: any, _index: number) => {
+  // 清空标识符
+  row.identifier = undefined
+  // 如果需要加载物模型数据
+  if (shouldShowIdentifierSelect(row) && row.productId) {
+    row.identifierLoading = true
+    await loadThingModel(row.productId)
+    row.identifierLoading = false
+  }
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    productId: undefined,
+    deviceId: undefined,
+    method: undefined,
+    identifier: undefined,
+    identifierLoading: false
+  }
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+/** 设置表单值 */
+const setData = (data: any[]) => {
+  // 确保每个项都有必要的字段
+  formData.value = (data || []).map((item) => ({
+    ...item,
+    identifierLoading: false
+  }))
+  // 为已有数据预加载物模型
+  data?.forEach(async (item) => {
+    if (item.productId && shouldShowIdentifierSelect(item)) {
+      await loadThingModel(item.productId)
+    }
+  })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await Promise.all([loadProductList(), loadDeviceList()])
+})
+
+defineExpose({ validate, getData, setData })
+</script>

+ 196 - 0
src/views/iot/rule/data/rule/index.vue

@@ -0,0 +1,196 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="规则名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入规则名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="规则状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择规则状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iot:data-rule:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      row-key="id"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+    >
+      <el-table-column label="规则编号" align="center" prop="id" />
+      <el-table-column label="规则名称" align="center" prop="name" />
+      <el-table-column label="规则描述" align="center" prop="description" />
+      <el-table-column label="规则状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="数据源" align="center" prop="sourceConfigs">
+        <template #default="scope"> {{ scope.row.sourceConfigs?.length || 0 }} 个 </template>
+      </el-table-column>
+      <el-table-column label="数据目的" align="center" prop="sinkIds">
+        <template #default="scope"> {{ scope.row.sinkIds?.length || 0 }} 个 </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iot:data-rule:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iot:data-rule:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <DataRuleForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DataRuleApi, DataRule } from '@/api/iot/rule/data/rule'
+import DataRuleForm from './DataRuleForm.vue'
+
+/** IoT 数据流转规则列表 */
+defineOptions({ name: 'IotDataRule' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<DataRule[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DataRuleApi.getDataRulePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DataRuleApi.deleteDataRule(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 42 - 61
src/views/iot/rule/databridge/IoTDataBridgeForm.vue

@@ -7,50 +7,41 @@
       :rules="formRules"
       label-width="120px"
     >
-      <el-form-item label="桥梁名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入桥梁名称" />
+      <el-form-item label="目的名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入目的名称" />
       </el-form-item>
-      <el-form-item label="桥梁方向" prop="direction">
-        <el-radio-group v-model="formData.direction">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_DIRECTION_ENUM)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
+      <el-form-item label="目的描述" prop="description">
+        <el-input v-model="formData.description" height="150px" type="textarea" />
       </el-form-item>
-      <el-form-item label="桥梁类型" prop="type">
-        <el-radio-group :model-value="formData.type" @change="handleTypeChange">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
+      <el-form-item label="目的类型" prop="type">
+        <el-select v-model="formData.type" @change="handleTypeChange">
+          <el-option
+            v-for="item in getIntDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM)"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
       </el-form-item>
-      <HttpConfigForm v-if="showConfig(IoTDataBridgeConfigType.HTTP)" v-model="formData.config" />
-      <MqttConfigForm v-if="showConfig(IoTDataBridgeConfigType.MQTT)" v-model="formData.config" />
+      <HttpConfigForm v-if="IotDataSinkTypeEnum.HTTP === formData.type" v-model="formData.config" />
+      <MqttConfigForm v-if="IotDataSinkTypeEnum.MQTT === formData.type" v-model="formData.config" />
       <RocketMQConfigForm
-        v-if="showConfig(IoTDataBridgeConfigType.ROCKETMQ)"
+        v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type"
         v-model="formData.config"
       />
       <KafkaMQConfigForm
-        v-if="showConfig(IoTDataBridgeConfigType.KAFKA)"
+        v-if="IotDataSinkTypeEnum.KAFKA === formData.type"
         v-model="formData.config"
       />
       <RabbitMQConfigForm
-        v-if="showConfig(IoTDataBridgeConfigType.RABBITMQ)"
+        v-if="IotDataSinkTypeEnum.RABBITMQ === formData.type"
         v-model="formData.config"
       />
-      <RedisStreamMQConfigForm
-        v-if="showConfig(IoTDataBridgeConfigType.REDIS_STREAM)"
+      <RedisStreamConfigForm
+        v-if="IotDataSinkTypeEnum.REDIS_STREAM === formData.type"
         v-model="formData.config"
       />
-      <el-form-item label="桥梁状态" prop="status">
+      <el-form-item label="目的状态" prop="status">
         <el-radio-group v-model="formData.status">
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -61,9 +52,6 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="桥梁描述" prop="description">
-        <el-input v-model="formData.description" height="150px" type="textarea" />
-      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -72,19 +60,20 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE, getDictObj, getIntDictOptions } from '@/utils/dict'
-import { DataBridgeApi, DataBridgeVO, IoTDataBridgeConfigType } from '@/api/iot/rule/databridge'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { DataSinkApi, DataSinkVO, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
 import {
   HttpConfigForm,
   KafkaMQConfigForm,
   MqttConfigForm,
   RabbitMQConfigForm,
-  RedisStreamMQConfigForm,
+  RedisStreamConfigForm,
   RocketMQConfigForm
 } from './config'
 
-/** IoT 数据桥梁的表单 */
-defineOptions({ name: 'IoTDataBridgeForm' })
+/** IoT 数据流转目的的表单 */
+defineOptions({ name: 'IoTDataSinkForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -93,25 +82,23 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref<DataBridgeVO>({
-  status: 0,
-  direction: 1, // TODO @puhui999:枚举类
-  type: 1, // TODO @puhui999:枚举类
+const formData = ref<DataSinkVO>({
+  status: CommonStatusEnum.ENABLE,
+  type: IotDataSinkTypeEnum.HTTP,
   config: {} as any
 })
 const formRules = reactive({
   // 通用字段
-  name: [{ required: true, message: '桥梁名称不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '桥梁状态不能为空', trigger: 'blur' }],
-  direction: [{ required: true, message: '桥梁方向不能为空', trigger: 'blur' }],
-  type: [{ required: true, message: '桥梁类型不能为空', trigger: 'change' }],
+  name: [{ required: true, message: '目的名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '目的状态不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '目的类型不能为空', trigger: 'change' }],
   // HTTP 配置
   'config.url': [{ required: true, message: '请求地址不能为空', trigger: 'blur' }],
   'config.method': [{ required: true, message: '请求方法不能为空', trigger: 'blur' }],
   // MQTT 配置
   'config.username': [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
   'config.password': [{ required: true, message: '密码不能为空', trigger: 'blur' }],
-  'config.clientId': [{ required: true, message: '客户端ID不能为空', trigger: 'blur' }],
+  'config.clientId': [{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' }],
   'config.topic': [{ required: true, message: '主题不能为空', trigger: 'blur' }],
   // RocketMQ 配置
   'config.nameServer': [{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }],
@@ -139,10 +126,6 @@ const formRules = reactive({
 })
 
 const formRef = ref() // 表单 Ref
-const showConfig = computed(() => (val: string) => {
-  const dict = getDictObj(DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM, formData.value.type)
-  return dict && dict.value + '' === val
-}) // 显示对应的 Config 配置项
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -154,7 +137,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await DataBridgeApi.getDataBridge(id)
+      formData.value = await DataSinkApi.getDataSink(id)
     } finally {
       formLoading.value = false
     }
@@ -170,12 +153,12 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as DataBridgeVO
+    const data = formData.value as unknown as DataSinkVO
     if (formType.value === 'create') {
-      await DataBridgeApi.createDataBridge(data)
+      await DataSinkApi.createDataSink(data)
       message.success(t('common.createSuccess'))
     } else {
-      await DataBridgeApi.updateDataBridge(data)
+      await DataSinkApi.updateDataSink(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -187,8 +170,8 @@ const submitForm = async () => {
 }
 
 /** 处理类型切换事件 */
-const handleTypeChange = (val: number) => {
-  formData.value.type = val
+const handleTypeChange = (type: number) => {
+  formData.value.type = type
   // 切换类型时重置配置
   formData.value.config = {} as any
 }
@@ -196,10 +179,8 @@ const handleTypeChange = (val: number) => {
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
-    // TODO @puhui999:换成枚举值哈
-    status: 0,
-    direction: 1,
-    type: 1,
+    status: CommonStatusEnum.ENABLE,
+    type: IotDataSinkTypeEnum.HTTP,
     config: {} as any
   }
   formRef.value?.resetFields()

+ 4 - 2
src/views/iot/rule/databridge/config/HttpConfigForm.vue

@@ -3,6 +3,7 @@
     <el-input v-model="urlPath" placeholder="请输入请求地址">
       <template #prepend>
         <el-select v-model="urlPrefix" placeholder="Select" style="width: 115px">
+          <!--suppress HttpUrlsUsage -->
           <el-option label="http://" value="http://" />
           <el-option label="https://" value="https://" />
         </el-select>
@@ -29,7 +30,7 @@
 </template>
 
 <script lang="ts" setup>
-import { HttpConfig, IoTDataBridgeConfigType } from '@/api/iot/rule/databridge'
+import { HttpConfig, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
 import { useVModel } from '@vueuse/core'
 import { isEmpty } from '@/utils/is'
 import KeyValueEditor from './components/KeyValueEditor.vue'
@@ -42,6 +43,7 @@ const props = defineProps<{
 const emit = defineEmits(['update:modelValue'])
 const config = useVModel(props, 'modelValue', emit) as Ref<HttpConfig>
 
+// noinspection HttpUrlsUsage
 /** URL处理 */
 const urlPrefix = ref('http://')
 const urlPath = ref('')
@@ -73,7 +75,7 @@ onMounted(() => {
   }
 
   config.value = {
-    type: IoTDataBridgeConfigType.HTTP,
+    type: IotDataSinkTypeEnum.HTTP + '', // 序列化成对应类型时使用
     url: '',
     method: 'POST',
     headers: {},

+ 2 - 2
src/views/iot/rule/databridge/config/KafkaMQConfigForm.vue

@@ -16,7 +16,7 @@
   </el-form-item>
 </template>
 <script lang="ts" setup>
-import { IoTDataBridgeConfigType, KafkaMQConfig } from '@/api/iot/rule/databridge'
+import { IotDataSinkTypeEnum, KafkaMQConfig } from '@/api/iot/rule/data/sink'
 import { useVModel } from '@vueuse/core'
 import { isEmpty } from '@/utils/is'
 
@@ -34,7 +34,7 @@ onMounted(() => {
     return
   }
   config.value = {
-    type: IoTDataBridgeConfigType.KAFKA,
+    type: IotDataSinkTypeEnum.KAFKA + '', // 序列化成对应类型时使用
     bootstrapServers: '',
     username: '',
     password: '',

+ 2 - 2
src/views/iot/rule/databridge/config/MqttConfigForm.vue

@@ -16,7 +16,7 @@
   </el-form-item>
 </template>
 <script lang="ts" setup>
-import { IoTDataBridgeConfigType, MqttConfig } from '@/api/iot/rule/databridge'
+import { IotDataSinkTypeEnum, MqttConfig } from '@/api/iot/rule/data/sink'
 import { useVModel } from '@vueuse/core'
 import { isEmpty } from '@/utils/is'
 
@@ -34,7 +34,7 @@ onMounted(() => {
     return
   }
   config.value = {
-    type: IoTDataBridgeConfigType.MQTT,
+    type: IotDataSinkTypeEnum.MQTT + '', // 序列化成对应类型时使用
     url: '',
     username: '',
     password: '',

+ 2 - 2
src/views/iot/rule/databridge/config/RabbitMQConfigForm.vue

@@ -31,7 +31,7 @@
   </el-form-item>
 </template>
 <script lang="ts" setup>
-import { IoTDataBridgeConfigType, RabbitMQConfig } from '@/api/iot/rule/databridge'
+import { IotDataSinkTypeEnum, RabbitMQConfig } from '@/api/iot/rule/data/sink'
 import { useVModel } from '@vueuse/core'
 import { isEmpty } from '@/utils/is'
 
@@ -49,7 +49,7 @@ onMounted(() => {
     return
   }
   config.value = {
-    type: IoTDataBridgeConfigType.RABBITMQ,
+    type: IotDataSinkTypeEnum.RABBITMQ + '', // 序列化成对应类型时使用
     host: '',
     port: 5672,
     virtualHost: '/',

+ 2 - 3
src/views/iot/rule/databridge/config/RedisStreamMQConfigForm.vue

@@ -1,4 +1,3 @@
-<!-- TODO @puhui999:去掉 MQ 关键字哈 -->
 <template>
   <el-form-item label="主机地址" prop="config.host">
     <el-input v-model="config.host" placeholder="请输入主机地址,如:localhost" />
@@ -29,7 +28,7 @@
   </el-form-item>
 </template>
 <script lang="ts" setup>
-import { IoTDataBridgeConfigType, RedisStreamMQConfig } from '@/api/iot/rule/databridge'
+import { IotDataSinkTypeEnum, RedisStreamMQConfig } from '@/api/iot/rule/data/sink'
 import { useVModel } from '@vueuse/core'
 import { isEmpty } from '@/utils/is'
 
@@ -47,7 +46,7 @@ onMounted(() => {
     return
   }
   config.value = {
-    type: IoTDataBridgeConfigType.REDIS_STREAM,
+    type: IotDataSinkTypeEnum.REDIS_STREAM + '', // 序列化成对应类型时使用
     host: '',
     port: 6379,
     password: '',

+ 2 - 2
src/views/iot/rule/databridge/config/RocketMQConfigForm.vue

@@ -27,7 +27,7 @@
   </el-form-item>
 </template>
 <script lang="ts" setup>
-import { IoTDataBridgeConfigType, RocketMQConfig } from '@/api/iot/rule/databridge'
+import { IotDataSinkTypeEnum, RocketMQConfig } from '@/api/iot/rule/data/sink'
 import { useVModel } from '@vueuse/core'
 import { isEmpty } from '@/utils/is'
 
@@ -45,7 +45,7 @@ onMounted(() => {
     return
   }
   config.value = {
-    type: IoTDataBridgeConfigType.ROCKETMQ,
+    type: IotDataSinkTypeEnum.ROCKETMQ + '', // 序列化成对应类型时使用
     nameServer: '',
     accessKey: '',
     secretKey: '',

+ 0 - 1
src/views/iot/rule/databridge/config/components/KeyValueEditor.vue

@@ -58,7 +58,6 @@ const updateModelValue = () => {
   emit('update:modelValue', result)
 }
 
-// TODO @puhui999:有告警的地方,尽量用 cursor 处理下
 /** 监听项目变化 */
 watch(items, updateModelValue, { deep: true })
 watch(

+ 2 - 2
src/views/iot/rule/databridge/config/index.ts

@@ -3,7 +3,7 @@ import MqttConfigForm from './MqttConfigForm.vue'
 import RocketMQConfigForm from './RocketMQConfigForm.vue'
 import KafkaMQConfigForm from './KafkaMQConfigForm.vue'
 import RabbitMQConfigForm from './RabbitMQConfigForm.vue'
-import RedisStreamMQConfigForm from './RedisStreamMQConfigForm.vue'
+import RedisStreamConfigForm from './RedisStreamConfigForm.vue'
 
 export {
   HttpConfigForm,
@@ -11,5 +11,5 @@ export {
   RocketMQConfigForm,
   KafkaMQConfigForm,
   RabbitMQConfigForm,
-  RedisStreamMQConfigForm
+  RedisStreamConfigForm
 }

+ 24 - 46
src/views/iot/rule/databridge/index.vue

@@ -8,21 +8,21 @@
       class="-mb-15px"
       label-width="68px"
     >
-      <el-form-item label="桥梁名称" prop="name">
+      <el-form-item label="目的名称" prop="name">
         <el-input
           v-model="queryParams.name"
           class="!w-240px"
           clearable
-          placeholder="请输入桥梁名称"
+          placeholder="请输入目的名称"
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="桥梁状态" prop="status">
+      <el-form-item label="目的状态" prop="status">
         <el-select
           v-model="queryParams.status"
           class="!w-240px"
           clearable
-          placeholder="请选择桥梁状态"
+          placeholder="请选择目的状态"
         >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@@ -32,30 +32,15 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="桥梁方向" prop="direction">
-        <el-select
-          v-model="queryParams.direction"
-          class="!w-240px"
-          clearable
-          placeholder="请选择桥梁方向"
-        >
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_DIRECTION_ENUM)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="桥梁类型" prop="type">
+      <el-form-item label="目的类型" prop="type">
         <el-select
           v-model="queryParams.type"
           class="!w-240px"
           clearable
-          placeholder="请选择桥梁类型"
+          placeholder="请选择目的类型"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -83,7 +68,7 @@
           重置
         </el-button>
         <el-button
-          v-hasPermi="['iot:data-bridge:create']"
+          v-hasPermi="['iot:data-sink:create']"
           plain
           type="primary"
           @click="openForm('create')"
@@ -98,22 +83,17 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="桥梁编号" prop="id" />
-      <el-table-column align="center" label="桥梁名称" prop="name" />
-      <el-table-column align="center" label="桥梁描述" prop="description" />
-      <el-table-column align="center" label="桥梁状态" prop="status">
+      <el-table-column align="center" label="目的编号" prop="id" />
+      <el-table-column align="center" label="目的名称" prop="name" />
+      <el-table-column align="center" label="目的描述" prop="description" />
+      <el-table-column align="center" label="目的状态" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="桥梁方向" prop="direction">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.IOT_DATA_BRIDGE_DIRECTION_ENUM" :value="scope.row.direction" />
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="桥梁类型" prop="type">
+      <el-table-column align="center" label="目的类型" prop="type">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM" :value="scope.row.type" />
+          <dict-tag :type="DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM" :value="scope.row.type" />
         </template>
       </el-table-column>
       <el-table-column
@@ -126,7 +106,7 @@
       <el-table-column align="center" fixed="right" label="操作" width="120px">
         <template #default="scope">
           <el-button
-            v-hasPermi="['iot:data-bridge:update']"
+            v-hasPermi="['iot:data-sink:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
@@ -134,7 +114,7 @@
             编辑
           </el-button>
           <el-button
-            v-hasPermi="['iot:data-bridge:delete']"
+            v-hasPermi="['iot:data-sink:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
@@ -154,31 +134,29 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <DataBridgeForm ref="formRef" @success="getList" />
+  <DataSinkForm ref="formRef" @success="getList" />
 </template>
 
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import { DataBridgeApi, DataBridgeVO } from '@/api/iot/rule/databridge'
-import DataBridgeForm from './IoTDataBridgeForm.vue'
+import { DataSinkApi, DataSinkVO } from '@/api/iot/rule/data/sink'
+import DataSinkForm from './DataSinkForm.vue'
 
-/** IoT 数据桥梁 列表 */
-defineOptions({ name: 'IotDataBridge' })
+/** IoT 数据流转目的 列表 */
+defineOptions({ name: 'IotDataSink' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const list = ref<DataBridgeVO[]>([]) // 列表的数据
+const list = ref<DataSinkVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: undefined,
-  description: undefined,
   status: undefined,
-  direction: undefined,
   type: undefined,
   createTime: []
 })
@@ -188,7 +166,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await DataBridgeApi.getDataBridgePage(queryParams)
+    const data = await DataSinkApi.getDataSinkPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -220,7 +198,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await DataBridgeApi.deleteDataBridge(id)
+    await DataSinkApi.deleteDataSink(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()

+ 330 - 0
src/views/iot/rule/scene/form/RuleSceneForm.vue

@@ -0,0 +1,330 @@
+<template>
+  <el-drawer
+    v-model="drawerVisible"
+    :title="drawerTitle"
+    size="80%"
+    direction="rtl"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    @close="handleClose"
+  >
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
+      <!-- 基础信息配置 -->
+      <BasicInfoSection v-model="formData" :rules="formRules" />
+      <!-- 触发器配置 -->
+      <TriggerSection v-model:triggers="formData.triggers" />
+      <!-- 执行器配置 -->
+      <ActionSection v-model:actions="formData.actions" />
+    </el-form>
+    <template #footer>
+      <div class="drawer-footer">
+        <el-button :disabled="submitLoading" type="primary" @click="handleSubmit">
+          <Icon icon="ep:check" />
+          确 定
+        </el-button>
+        <el-button @click="handleClose">
+          <Icon icon="ep:close" />
+          取 消
+        </el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import BasicInfoSection from './sections/BasicInfoSection.vue'
+import TriggerSection from './sections/TriggerSection.vue'
+import ActionSection from './sections/ActionSection.vue'
+import { IotSceneRule } from '@/api/iot/rule/scene'
+import { RuleSceneApi } from '@/api/iot/rule/scene'
+import {
+  IotRuleSceneTriggerTypeEnum,
+  IotRuleSceneActionTypeEnum,
+  isDeviceTrigger
+} from '@/views/iot/utils/constants'
+import { ElMessage } from 'element-plus'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** IoT 场景联动规则表单 - 主表单组件 */
+defineOptions({ name: 'RuleSceneForm' })
+
+/** 组件属性定义 */
+const props = defineProps<{
+  /** 抽屉显示状态 */
+  modelValue: boolean
+  /** 编辑的场景联动规则数据 */
+  ruleScene?: IotSceneRule
+}>()
+
+/** 组件事件定义 */
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'success'): void
+}>()
+
+const drawerVisible = useVModel(props, 'modelValue', emit) // 抽屉显示状态
+
+/**
+ * 创建默认的表单数据
+ * @returns 默认表单数据对象
+ */
+const createDefaultFormData = (): IotSceneRule => {
+  return {
+    name: '',
+    description: '',
+    status: CommonStatusEnum.ENABLE, // 默认启用状态
+    triggers: [
+      {
+        type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+        productId: undefined,
+        deviceId: undefined,
+        identifier: undefined,
+        operator: undefined,
+        value: undefined,
+        cronExpression: undefined,
+        conditionGroups: [] // 空的条件组数组
+      }
+    ],
+    actions: []
+  }
+}
+
+const formRef = ref() // 表单引用
+const formData = ref<IotSceneRule>(createDefaultFormData()) // 表单数据
+
+/**
+ * 触发器校验器
+ * @param _rule 校验规则(未使用)
+ * @param value 校验值
+ * @param callback 回调函数
+ */
+const validateTriggers = (_rule: any, value: any, callback: any) => {
+  if (!value || !Array.isArray(value) || value.length === 0) {
+    callback(new Error('至少需要一个触发器'))
+    return
+  }
+
+  for (let i = 0; i < value.length; i++) {
+    const trigger = value[i]
+
+    // 校验触发器类型
+    if (!trigger.type) {
+      callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
+      return
+    }
+
+    // 校验设备触发器
+    if (isDeviceTrigger(trigger.type)) {
+      if (!trigger.productId) {
+        callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
+        return
+      }
+      if (!trigger.deviceId) {
+        callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
+        return
+      }
+      if (!trigger.identifier) {
+        callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
+        return
+      }
+      if (!trigger.operator) {
+        callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
+        return
+      }
+      if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
+        callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
+        return
+      }
+    }
+
+    // 校验定时触发器
+    if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
+      if (!trigger.cronExpression) {
+        callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
+        return
+      }
+    }
+  }
+
+  callback()
+}
+
+/**
+ * 执行器校验器
+ * @param _rule 校验规则(未使用)
+ * @param value 校验值
+ * @param callback 回调函数
+ */
+const validateActions = (_rule: any, value: any, callback: any) => {
+  if (!value || !Array.isArray(value) || value.length === 0) {
+    callback(new Error('至少需要一个执行器'))
+    return
+  }
+
+  for (let i = 0; i < value.length; i++) {
+    const action = value[i]
+
+    // 校验执行器类型
+    if (!action.type) {
+      callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
+      return
+    }
+
+    // 校验设备控制执行器
+    if (
+      action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
+      action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+    ) {
+      if (!action.productId) {
+        callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
+        return
+      }
+      if (!action.deviceId) {
+        callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
+        return
+      }
+
+      // 服务调用需要验证服务标识符
+      if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
+        if (!action.identifier) {
+          callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
+          return
+        }
+      }
+
+      if (!action.params || Object.keys(action.params).length === 0) {
+        callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
+        return
+      }
+    }
+
+    // 校验告警执行器
+    if (
+      action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
+      action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
+    ) {
+      if (!action.alertConfigId) {
+        callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
+        return
+      }
+    }
+  }
+
+  callback()
+}
+
+const formRules = reactive({
+  name: [
+    { required: true, message: '场景名称不能为空', trigger: 'blur' },
+    { type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
+  ],
+  status: [
+    { required: true, message: '场景状态不能为空', trigger: 'change' },
+    {
+      type: 'enum',
+      enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
+      message: '状态值必须为启用或禁用',
+      trigger: 'change'
+    }
+  ],
+  description: [
+    { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
+  ],
+  triggers: [{ required: true, validator: validateTriggers, trigger: 'change' }],
+  actions: [{ required: true, validator: validateActions, trigger: 'change' }]
+}) // 表单校验规则
+
+const submitLoading = ref(false) // 提交加载状态
+const isEdit = ref(false) // 是否为编辑模式
+const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则')) // 抽屉标题
+
+/** 提交表单 */
+const handleSubmit = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  // 提交请求
+  submitLoading.value = true
+  try {
+    if (isEdit.value) {
+      // 更新场景联动规则
+      await RuleSceneApi.updateRuleScene(formData.value)
+      ElMessage.success('更新成功')
+    } else {
+      // 创建场景联动规则
+      await RuleSceneApi.createRuleScene(formData.value)
+      ElMessage.success('创建成功')
+    }
+
+    // 关闭抽屉并触发成功事件
+    drawerVisible.value = false
+    emit('success')
+  } catch (error) {
+    console.error('保存失败:', error)
+    ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+/** 处理抽屉关闭事件 */
+const handleClose = () => {
+  drawerVisible.value = false
+}
+
+/** 初始化表单数据 */
+const initFormData = () => {
+  if (props.ruleScene) {
+    // 编辑模式:数据结构已对齐,直接使用后端数据
+    isEdit.value = true
+    formData.value = {
+      ...props.ruleScene,
+      // 确保触发器数组不为空
+      triggers: props.ruleScene.triggers?.length
+        ? props.ruleScene.triggers
+        : [
+            {
+              type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+              productId: undefined,
+              deviceId: undefined,
+              identifier: undefined,
+              operator: undefined,
+              value: undefined,
+              cronExpression: undefined,
+              conditionGroups: []
+            }
+          ],
+      // 确保执行器数组不为空
+      actions: props.ruleScene.actions || []
+    }
+  } else {
+    // 新增模式:使用默认数据
+    isEdit.value = false
+    formData.value = createDefaultFormData()
+  }
+}
+
+/** 监听抽屉显示 */
+watch(drawerVisible, async (visible) => {
+  if (visible) {
+    initFormData()
+    // 重置表单验证状态
+    await nextTick()
+    formRef.value?.clearValidate()
+  }
+})
+
+/** 监听编辑数据变化 */
+watch(
+  () => props.ruleScene,
+  () => {
+    if (drawerVisible.value) {
+      initFormData()
+    }
+  },
+  { deep: true }
+)
+</script>

+ 81 - 0
src/views/iot/rule/scene/form/configs/AlertConfig.vue

@@ -0,0 +1,81 @@
+<!-- 告警配置组件 -->
+<template>
+  <div class="w-full">
+    <el-form-item label="告警配置" required>
+      <el-select
+        v-model="localValue"
+        placeholder="请选择告警配置"
+        filterable
+        clearable
+        @change="handleChange"
+        class="w-full"
+        :loading="loading"
+      >
+        <el-option
+          v-for="config in alertConfigs"
+          :key="config.id"
+          :label="config.name"
+          :value="config.id"
+        >
+          <div class="flex items-center justify-between">
+            <span>{{ config.name }}</span>
+            <el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
+              {{ config.enabled ? '启用' : '禁用' }}
+            </el-tag>
+          </div>
+        </el-option>
+      </el-select>
+    </el-form-item>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { AlertConfigApi } from '@/api/iot/alert/config'
+
+/** 告警配置组件 */
+defineOptions({ name: 'AlertConfig' })
+
+const props = defineProps<{
+  modelValue?: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value?: number): void
+}>()
+
+const localValue = useVModel(props, 'modelValue', emit)
+
+const loading = ref(false) // 加载状态
+const alertConfigs = ref<any[]>([]) // 告警配置列表
+
+/**
+ * 处理选择变化事件
+ * @param value 选中的值
+ */
+const handleChange = (value?: number) => {
+  emit('update:modelValue', value)
+}
+
+/**
+ * 加载告警配置列表
+ */
+const loadAlertConfigs = async () => {
+  loading.value = true
+  try {
+    const data = await AlertConfigApi.getAlertConfigPage({
+      pageNo: 1,
+      pageSize: 100,
+      enabled: true // 只加载启用的配置
+    })
+    alertConfigs.value = data.list || []
+  } finally {
+    loading.value = false
+  }
+}
+
+// 组件挂载时加载数据
+onMounted(() => {
+  loadAlertConfigs()
+})
+</script>

+ 301 - 0
src/views/iot/rule/scene/form/configs/ConditionConfig.vue

@@ -0,0 +1,301 @@
+<!-- 单个条件配置组件 -->
+<template>
+  <div class="flex flex-col gap-16px">
+    <!-- 条件类型选择 -->
+    <el-row :gutter="16">
+      <el-col :span="8">
+        <el-form-item label="条件类型" required>
+          <el-select
+            :model-value="condition.type"
+            @update:model-value="(value) => updateConditionField('type', value)"
+            @change="handleConditionTypeChange"
+            placeholder="请选择条件类型"
+            class="w-full"
+          >
+            <el-option
+              v-for="option in getConditionTypeOptions()"
+              :key="option.value"
+              :label="option.label"
+              :value="option.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 产品设备选择 - 设备相关条件的公共部分 -->
+    <el-row v-if="isDeviceCondition" :gutter="16">
+      <el-col :span="12">
+        <el-form-item label="产品" required>
+          <ProductSelector
+            :model-value="condition.productId"
+            @update:model-value="(value) => updateConditionField('productId', value)"
+            @change="handleProductChange"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="设备" required>
+          <DeviceSelector
+            :model-value="condition.deviceId"
+            @update:model-value="(value) => updateConditionField('deviceId', value)"
+            :product-id="condition.productId"
+            @change="handleDeviceChange"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 设备状态条件配置 -->
+    <div
+      v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS"
+      class="flex flex-col gap-16px"
+    >
+      <!-- 状态和操作符选择 -->
+      <el-row :gutter="16">
+        <!-- 操作符选择 -->
+        <el-col :span="12">
+          <el-form-item label="操作符" required>
+            <el-select
+              :model-value="condition.operator"
+              @update:model-value="(value) => updateConditionField('operator', value)"
+              placeholder="请选择操作符"
+              class="w-full"
+            >
+              <el-option
+                v-for="option in statusOperatorOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+
+        <!-- 状态选择 -->
+        <el-col :span="12">
+          <el-form-item label="设备状态" required>
+            <el-select
+              :model-value="condition.param"
+              @update:model-value="(value) => updateConditionField('param', value)"
+              placeholder="请选择设备状态"
+              class="w-full"
+            >
+              <el-option
+                v-for="option in deviceStatusOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 设备属性条件配置 -->
+    <div
+      v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY"
+      class="space-y-16px"
+    >
+      <!-- 属性配置 -->
+      <el-row :gutter="16">
+        <!-- 属性/事件/服务选择 -->
+        <el-col :span="6">
+          <el-form-item label="监控项" required>
+            <PropertySelector
+              :model-value="condition.identifier"
+              @update:model-value="(value) => updateConditionField('identifier', value)"
+              :trigger-type="triggerType"
+              :product-id="condition.productId"
+              :device-id="condition.deviceId"
+              @change="handlePropertyChange"
+            />
+          </el-form-item>
+        </el-col>
+
+        <!-- 操作符选择 -->
+        <el-col :span="6">
+          <el-form-item label="操作符" required>
+            <OperatorSelector
+              :model-value="condition.operator"
+              @update:model-value="(value) => updateConditionField('operator', value)"
+              :property-type="propertyType"
+              @change="handleOperatorChange"
+            />
+          </el-form-item>
+        </el-col>
+
+        <!-- 值输入 -->
+        <el-col :span="12">
+          <el-form-item label="比较值" required>
+            <ValueInput
+              :model-value="condition.param"
+              @update:model-value="(value) => updateConditionField('param', value)"
+              :property-type="propertyType"
+              :operator="condition.operator"
+              :property-config="propertyConfig"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 当前时间条件配置 -->
+    <CurrentTimeConditionConfig
+      v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME"
+      :model-value="condition"
+      @update:model-value="updateCondition"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import PropertySelector from '../selectors/PropertySelector.vue'
+import OperatorSelector from '../selectors/OperatorSelector.vue'
+import ValueInput from '../inputs/ValueInput.vue'
+import type { TriggerCondition } from '@/api/iot/rule/scene'
+import {
+  IotRuleSceneTriggerConditionTypeEnum,
+  IotRuleSceneTriggerConditionParameterOperatorEnum,
+  getConditionTypeOptions,
+  IoTDeviceStatusEnum
+} from '@/views/iot/utils/constants'
+
+/** 单个条件配置组件 */
+defineOptions({ name: 'ConditionConfig' })
+
+const props = defineProps<{
+  modelValue: TriggerCondition
+  triggerType: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: TriggerCondition): void
+}>()
+
+/** 获取设备状态选项 */
+const deviceStatusOptions = [
+  {
+    value: IoTDeviceStatusEnum.ONLINE.value,
+    label: IoTDeviceStatusEnum.ONLINE.label
+  },
+  {
+    value: IoTDeviceStatusEnum.OFFLINE.value,
+    label: IoTDeviceStatusEnum.OFFLINE.label
+  }
+]
+
+/** 获取状态操作符选项 */
+const statusOperatorOptions = [
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name
+  }
+]
+
+const condition = useVModel(props, 'modelValue', emit)
+
+const propertyType = ref<string>('string') // 属性类型
+const propertyConfig = ref<any>(null) // 属性配置
+const isDeviceCondition = computed(() => {
+  return (
+    condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
+    condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
+  )
+}) // 计算属性:判断是否为设备相关条件
+
+/**
+ * 更新条件字段
+ * @param field 字段名
+ * @param value 字段值
+ */
+const updateConditionField = (field: any, value: any) => {
+  ;(condition.value as any)[field] = value
+  emit('update:modelValue', condition.value)
+}
+
+/**
+ * 更新整个条件对象
+ * @param newCondition 新的条件对象
+ */
+const updateCondition = (newCondition: TriggerCondition) => {
+  condition.value = newCondition
+  emit('update:modelValue', condition.value)
+}
+
+/**
+ * 处理条件类型变化事件
+ * @param type 条件类型
+ */
+const handleConditionTypeChange = (type: number) => {
+  // 根据条件类型清理字段
+  const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
+  const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
+
+  // 清理标识符字段(时间条件和设备状态条件都不需要)
+  if (isCurrentTime || isDeviceStatus) {
+    condition.value.identifier = undefined
+  }
+
+  // 清理设备相关字段(仅时间条件需要)
+  if (isCurrentTime) {
+    condition.value.productId = undefined
+    condition.value.deviceId = undefined
+  }
+
+  // 设置默认操作符
+  condition.value.operator = isCurrentTime
+    ? 'at_time'
+    : IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
+
+  // 清空参数值
+  condition.value.param = ''
+}
+
+/** 处理产品变化事件 */
+const handleProductChange = (_: number) => {
+  // 产品变化时清空设备和属性
+  condition.value.deviceId = undefined
+  condition.value.identifier = ''
+}
+
+/** 处理设备变化事件 */
+const handleDeviceChange = (_: number) => {
+  // 设备变化时清空属性
+  condition.value.identifier = ''
+}
+
+/**
+ * 处理属性变化事件
+ * @param propertyInfo 属性信息对象
+ */
+const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
+  propertyType.value = propertyInfo.type
+  propertyConfig.value = propertyInfo.config
+
+  // 重置操作符和值
+  condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
+  condition.value.param = ''
+}
+
+/** 处理操作符变化事件 */
+const handleOperatorChange = () => {
+  // 重置值
+  condition.value.param = ''
+}
+</script>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>

+ 234 - 0
src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue

@@ -0,0 +1,234 @@
+<!-- 当前时间条件配置组件 -->
+<template>
+  <div class="flex flex-col gap-16px">
+    <el-row :gutter="16">
+      <!-- 时间操作符选择 -->
+      <el-col :span="8">
+        <el-form-item label="时间条件" required>
+          <el-select
+            :model-value="condition.operator"
+            @update:model-value="(value) => updateConditionField('operator', value)"
+            placeholder="请选择时间条件"
+            class="w-full"
+          >
+            <el-option
+              v-for="option in timeOperatorOptions"
+              :key="option.value"
+              :label="option.label"
+              :value="option.value"
+            >
+              <div class="flex items-center justify-between w-full">
+                <div class="flex items-center gap-8px">
+                  <Icon :icon="option.icon" :class="option.iconClass" />
+                  <span>{{ option.label }}</span>
+                </div>
+                <el-tag :type="option.tag as any" size="small">{{ option.category }}</el-tag>
+              </div>
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-col>
+
+      <!-- 时间值输入 -->
+      <el-col :span="8">
+        <el-form-item label="时间值" required>
+          <el-time-picker
+            v-if="needsTimeInput"
+            :model-value="timeValue"
+            @update:model-value="handleTimeValueChange"
+            placeholder="请选择时间"
+            format="HH:mm:ss"
+            value-format="HH:mm:ss"
+            class="w-full"
+          />
+          <el-date-picker
+            v-else-if="needsDateInput"
+            :model-value="timeValue"
+            @update:model-value="handleTimeValueChange"
+            type="datetime"
+            placeholder="请选择日期时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            class="w-full"
+          />
+          <div v-else class="text-[var(--el-text-color-placeholder)] text-14px">
+            无需设置时间值
+          </div>
+        </el-form-item>
+      </el-col>
+
+      <!-- 第二个时间值(范围条件) -->
+      <el-col :span="8" v-if="needsSecondTimeInput">
+        <el-form-item label="结束时间" required>
+          <el-time-picker
+            v-if="needsTimeInput"
+            :model-value="timeValue2"
+            @update:model-value="handleTimeValue2Change"
+            placeholder="请选择结束时间"
+            format="HH:mm:ss"
+            value-format="HH:mm:ss"
+            class="w-full"
+          />
+          <el-date-picker
+            v-else
+            :model-value="timeValue2"
+            @update:model-value="handleTimeValue2Change"
+            type="datetime"
+            placeholder="请选择结束日期时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            class="w-full"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { IotRuleSceneTriggerTimeOperatorEnum } from '@/views/iot/utils/constants'
+import type { TriggerCondition } from '@/api/iot/rule/scene'
+
+/** 当前时间条件配置组件 */
+defineOptions({ name: 'CurrentTimeConditionConfig' })
+
+const props = defineProps<{
+  modelValue: TriggerCondition
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: TriggerCondition): void
+}>()
+
+const condition = useVModel(props, 'modelValue', emit)
+
+// 时间操作符选项
+const timeOperatorOptions = [
+  {
+    value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
+    label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name,
+    icon: 'ep:arrow-left',
+    iconClass: 'text-blue-500',
+    tag: 'primary',
+    category: '时间点'
+  },
+  {
+    value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
+    label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name,
+    icon: 'ep:arrow-right',
+    iconClass: 'text-green-500',
+    tag: 'success',
+    category: '时间点'
+  },
+  {
+    value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
+    label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name,
+    icon: 'ep:sort',
+    iconClass: 'text-orange-500',
+    tag: 'warning',
+    category: '时间段'
+  },
+  {
+    value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
+    label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name,
+    icon: 'ep:position',
+    iconClass: 'text-purple-500',
+    tag: 'info',
+    category: '时间点'
+  },
+  {
+    value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value,
+    label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name,
+    icon: 'ep:calendar',
+    iconClass: 'text-red-500',
+    tag: 'danger',
+    category: '日期'
+  }
+]
+
+// 计算属性:是否需要时间输入
+const needsTimeInput = computed(() => {
+  const timeOnlyOperators = [
+    IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
+    IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
+    IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
+    IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value
+  ]
+  return timeOnlyOperators.includes(condition.value.operator as any)
+})
+
+// 计算属性:是否需要日期输入
+const needsDateInput = computed(() => {
+  return false // 暂时不支持日期输入,只支持时间
+})
+
+// 计算属性:是否需要第二个时间输入
+const needsSecondTimeInput = computed(() => {
+  return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
+})
+
+// 计算属性:从 param 中解析时间值
+const timeValue = computed(() => {
+  if (!condition.value.param) return ''
+  const params = condition.value.param.split(',')
+  return params[0] || ''
+})
+
+// 计算属性:从 param 中解析第二个时间值
+const timeValue2 = computed(() => {
+  if (!condition.value.param) return ''
+  const params = condition.value.param.split(',')
+  return params[1] || ''
+})
+
+/**
+ * 更新条件字段
+ * @param field 字段名
+ * @param value 字段值
+ */
+const updateConditionField = (field: any, value: any) => {
+  condition.value[field] = value
+}
+
+/**
+ * 处理第一个时间值变化
+ * @param value 时间值
+ */
+const handleTimeValueChange = (value: string) => {
+  const currentParams = condition.value.param ? condition.value.param.split(',') : []
+  currentParams[0] = value || ''
+
+  // 如果是范围条件,保留第二个值;否则只保留第一个值
+  if (needsSecondTimeInput.value) {
+    condition.value.param = currentParams.slice(0, 2).join(',')
+  } else {
+    condition.value.param = currentParams[0]
+  }
+}
+
+/**
+ * 处理第二个时间值变化
+ * @param value 时间值
+ */
+const handleTimeValue2Change = (value: string) => {
+  const currentParams = condition.value.param ? condition.value.param.split(',') : ['']
+  currentParams[1] = value || ''
+  condition.value.param = currentParams.slice(0, 2).join(',')
+}
+
+/** 监听操作符变化,清理不相关的时间值 */
+watch(
+  () => condition.value.operator,
+  (newOperator) => {
+    if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
+      // 今日条件不需要时间参数
+      condition.value.param = ''
+    } else if (!needsSecondTimeInput.value) {
+      // 非范围条件只保留第一个时间值
+      const currentParams = condition.value.param ? condition.value.param.split(',') : []
+      condition.value.param = currentParams[0] || ''
+    }
+  }
+)
+</script>

+ 376 - 0
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue

@@ -0,0 +1,376 @@
+<!-- 设备控制配置组件 -->
+<template>
+  <div class="flex flex-col gap-16px">
+    <!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
+    <el-row :gutter="16">
+      <el-col :span="12">
+        <el-form-item label="产品" required>
+          <ProductSelector v-model="action.productId" @change="handleProductChange" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="设备" required>
+          <DeviceSelector
+            v-model="action.deviceId"
+            :product-id="action.productId"
+            @change="handleDeviceChange"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 服务选择 - 服务调用类型时显示 -->
+    <div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
+      <el-form-item label="服务" required>
+        <el-select
+          v-model="action.identifier"
+          placeholder="请选择服务"
+          filterable
+          clearable
+          class="w-full"
+          :loading="loadingServices"
+          @change="handleServiceChange"
+        >
+          <el-option
+            v-for="service in serviceList"
+            :key="service.identifier"
+            :label="service.name"
+            :value="service.identifier"
+          >
+            <div class="flex items-center justify-between">
+              <span>{{ service.name }}</span>
+              <el-tag :type="service.callType === 'sync' ? 'primary' : 'success'" size="small">
+                {{ service.callType === 'sync' ? '同步' : '异步' }}
+              </el-tag>
+            </div>
+          </el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- 服务参数配置 -->
+      <div v-if="action.identifier" class="space-y-16px">
+        <el-form-item label="服务参数" required>
+          <JsonParamsInput
+            v-model="paramsValue"
+            type="service"
+            :config="{ service: selectedService } as any"
+            placeholder="请输入 JSON 格式的服务参数"
+          />
+        </el-form-item>
+      </div>
+    </div>
+
+    <!-- 控制参数配置 - 属性设置类型时显示 -->
+    <div v-if="action.productId && isPropertySetAction" class="space-y-16px">
+      <!-- 参数配置 -->
+      <el-form-item label="参数" required>
+        <JsonParamsInput
+          v-model="paramsValue"
+          type="property"
+          :config="{ properties: thingModelProperties }"
+          placeholder="请输入 JSON 格式的控制参数"
+        />
+      </el-form-item>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import JsonParamsInput from '../inputs/JsonParamsInput.vue'
+import type { Action } from '@/api/iot/rule/scene'
+import type { ThingModelProperty, ThingModelService } from '@/api/iot/thingmodel'
+import {
+  IotRuleSceneActionTypeEnum,
+  IoTThingModelAccessModeEnum,
+  IoTDataSpecsDataTypeEnum
+} from '@/views/iot/utils/constants'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+
+/** 设备控制配置组件 */
+defineOptions({ name: 'DeviceControlConfig' })
+
+const props = defineProps<{
+  modelValue: Action
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: Action): void
+}>()
+
+const action = useVModel(props, 'modelValue', emit)
+
+const thingModelProperties = ref<ThingModelProperty[]>([]) // 物模型属性列表
+const loadingThingModel = ref(false) // 物模型加载状态
+const selectedService = ref<ThingModelService | null>(null) // 选中的服务对象
+const serviceList = ref<ThingModelService[]>([]) // 服务列表
+const loadingServices = ref(false) // 服务加载状态
+
+// 参数值的计算属性,用于双向绑定
+const paramsValue = computed({
+  get: () => {
+    // 如果 params 是对象,转换为 JSON 字符串(兼容旧数据)
+    if (action.value.params && typeof action.value.params === 'object') {
+      return JSON.stringify(action.value.params, null, 2)
+    }
+    // 如果 params 已经是字符串,直接返回
+    return action.value.params || ''
+  },
+  set: (value: string) => {
+    // 直接保存为 JSON 字符串,不进行解析转换
+    action.value.params = value.trim() || ''
+  }
+})
+
+// 计算属性:是否为属性设置类型
+const isPropertySetAction = computed(() => {
+  return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+})
+
+// 计算属性:是否为服务调用类型
+const isServiceInvokeAction = computed(() => {
+  return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+})
+
+/**
+ * 处理产品变化事件
+ * @param productId 产品 ID
+ */
+const handleProductChange = (productId?: number) => {
+  // 当产品变化时,清空设备选择和参数配置
+  if (action.value.productId !== productId) {
+    action.value.deviceId = undefined
+    action.value.identifier = undefined // 清空服务标识符
+    action.value.params = '' // 清空参数,保存为空字符串
+    selectedService.value = null // 清空选中的服务
+    serviceList.value = [] // 清空服务列表
+  }
+
+  // 加载新产品的物模型属性或服务列表
+  if (productId) {
+    if (isPropertySetAction.value) {
+      loadThingModelProperties(productId)
+    } else if (isServiceInvokeAction.value) {
+      loadServiceList(productId)
+    }
+  }
+}
+
+/**
+ * 处理设备变化事件
+ * @param deviceId 设备 ID
+ */
+const handleDeviceChange = (deviceId?: number) => {
+  // 当设备变化时,清空参数配置
+  if (action.value.deviceId !== deviceId) {
+    action.value.params = '' // 清空参数,保存为空字符串
+  }
+}
+
+/**
+ * 处理服务变化事件
+ * @param serviceIdentifier 服务标识符
+ */
+const handleServiceChange = (serviceIdentifier?: string) => {
+  // 根据服务标识符找到对应的服务对象
+  const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
+  selectedService.value = service
+
+  // 当服务变化时,清空参数配置
+  action.value.params = ''
+
+  // 如果选择了服务且有输入参数,生成默认参数结构
+  if (service && service.inputParams && service.inputParams.length > 0) {
+    const defaultParams = {}
+    service.inputParams.forEach((param) => {
+      defaultParams[param.identifier] = getDefaultValueForParam(param)
+    })
+    // 将默认参数转换为 JSON 字符串保存
+    action.value.params = JSON.stringify(defaultParams, null, 2)
+  }
+}
+
+/**
+ * 获取物模型TSL数据
+ * @param productId 产品ID
+ * @returns 物模型TSL数据
+ */
+const getThingModelTSL = async (productId: number) => {
+  if (!productId) return null
+
+  try {
+    return await ThingModelApi.getThingModelTSLByProductId(productId)
+  } catch (error) {
+    console.error('获取物模型TSL数据失败:', error)
+    return null
+  }
+}
+
+/**
+ * 加载物模型属性(可写属性)
+ * @param productId 产品ID
+ */
+const loadThingModelProperties = async (productId: number) => {
+  if (!productId) {
+    thingModelProperties.value = []
+    return
+  }
+
+  try {
+    loadingThingModel.value = true
+    const tslData = await getThingModelTSL(productId)
+
+    if (!tslData?.properties) {
+      thingModelProperties.value = []
+      return
+    }
+
+    // 过滤出可写的属性(accessMode 包含 'w')
+    thingModelProperties.value = tslData.properties.filter(
+      (property: ThingModelProperty) =>
+        property.accessMode &&
+        (property.accessMode === IoTThingModelAccessModeEnum.READ_WRITE.value ||
+          property.accessMode === IoTThingModelAccessModeEnum.WRITE_ONLY.value)
+    )
+  } catch (error) {
+    console.error('加载物模型属性失败:', error)
+    thingModelProperties.value = []
+  } finally {
+    loadingThingModel.value = false
+  }
+}
+
+/**
+ * 加载服务列表
+ * @param productId 产品ID
+ */
+const loadServiceList = async (productId: number) => {
+  if (!productId) {
+    serviceList.value = []
+    return
+  }
+
+  try {
+    loadingServices.value = true
+    const tslData = await getThingModelTSL(productId)
+
+    if (!tslData?.services) {
+      serviceList.value = []
+      return
+    }
+
+    serviceList.value = tslData.services
+  } catch (error) {
+    console.error('加载服务列表失败:', error)
+    serviceList.value = []
+  } finally {
+    loadingServices.value = false
+  }
+}
+
+/**
+ * 从TSL加载服务信息(用于编辑模式回显)
+ * @param productId 产品ID
+ * @param serviceIdentifier 服务标识符
+ */
+const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
+  // 先加载服务列表
+  await loadServiceList(productId)
+
+  // 然后设置选中的服务
+  const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
+  if (service) {
+    selectedService.value = service
+  }
+}
+
+/**
+ * 根据参数类型获取默认值
+ * @param param 参数对象
+ * @returns 默认值
+ */
+const getDefaultValueForParam = (param: any) => {
+  switch (param.dataType) {
+    case IoTDataSpecsDataTypeEnum.INT:
+      return 0
+    case IoTDataSpecsDataTypeEnum.FLOAT:
+    case IoTDataSpecsDataTypeEnum.DOUBLE:
+      return 0.0
+    case IoTDataSpecsDataTypeEnum.BOOL:
+      return false
+    case IoTDataSpecsDataTypeEnum.TEXT:
+      return ''
+    case IoTDataSpecsDataTypeEnum.ENUM:
+      // 如果有枚举值,使用第一个
+      if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
+        return param.dataSpecs.dataSpecsList[0].value
+      }
+      return ''
+    default:
+      return ''
+  }
+}
+
+const isInitialized = ref(false) // 防止重复初始化的标志
+
+/**
+ * 初始化组件数据
+ */
+const initializeComponent = async () => {
+  if (isInitialized.value) return
+
+  const currentAction = action.value
+  if (!currentAction) return
+
+  // 如果已经选择了产品且是属性设置类型,加载物模型
+  if (currentAction.productId && isPropertySetAction.value) {
+    await loadThingModelProperties(currentAction.productId)
+  }
+
+  // 如果是服务调用类型且已有标识符,初始化服务选择
+  if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
+    // 加载物模型TSL以获取服务信息
+    await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
+  }
+
+  isInitialized.value = true
+}
+
+/** 组件初始化 */
+onMounted(() => {
+  initializeComponent()
+})
+
+/** 监听关键字段的变化,避免深度监听导致的性能问题 */
+watch(
+  () => [action.value.productId, action.value.type, action.value.identifier],
+  async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
+    // 避免初始化时的重复调用
+    if (!isInitialized.value) return
+
+    // 产品变化时重新加载数据
+    if (newProductId !== oldProductId) {
+      if (newProductId && isPropertySetAction.value) {
+        await loadThingModelProperties(newProductId as number)
+      } else if (newProductId && isServiceInvokeAction.value) {
+        await loadServiceList(newProductId as number)
+      }
+    }
+
+    // 服务标识符变化时更新选中的服务
+    if (
+      newIdentifier !== oldIdentifier &&
+      newProductId &&
+      isServiceInvokeAction.value &&
+      newIdentifier
+    ) {
+      const service = serviceList.value.find((s: any) => s.identifier === newIdentifier)
+      if (service) {
+        selectedService.value = service
+      }
+    }
+  }
+)
+</script>

+ 251 - 0
src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue

@@ -0,0 +1,251 @@
+<!-- 设备触发配置组件 -->
+<template>
+  <div class="flex flex-col gap-16px">
+    <!-- 主条件配置 - 默认直接展示 -->
+    <div class="space-y-16px">
+      <!-- 主条件配置 -->
+      <div class="flex flex-col gap-16px">
+        <!-- 主条件配置 -->
+        <div class="space-y-16px">
+          <!-- 主条件头部 - 与附加条件组保持一致的绿色风格 -->
+          <div
+            class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
+          >
+            <div class="flex items-center gap-12px">
+              <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+                <div
+                  class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+                >
+                  主
+                </div>
+                <span>主条件</span>
+              </div>
+              <el-tag size="small" type="success">必须满足</el-tag>
+            </div>
+          </div>
+
+          <!-- 主条件内容配置 -->
+          <MainConditionInnerConfig
+            :model-value="trigger"
+            @update:model-value="updateCondition"
+            :trigger-type="trigger.type"
+            @trigger-type-change="handleTriggerTypeChange"
+          />
+        </div>
+      </div>
+    </div>
+
+    <!-- 条件组配置 -->
+    <div class="space-y-16px">
+      <!-- 条件组配置 -->
+      <div class="flex flex-col gap-16px">
+        <!-- 条件组容器头部 -->
+        <div
+          class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
+        >
+          <div class="flex items-center gap-12px">
+            <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+              <div
+                class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+              >
+                组
+              </div>
+              <span>附加条件组</span>
+            </div>
+            <el-tag size="small" type="success">与"主条件"为且关系</el-tag>
+            <el-tag size="small" type="info">
+              {{ trigger.conditionGroups?.length || 0 }} 个子条件组
+            </el-tag>
+          </div>
+          <div class="flex items-center gap-8px">
+            <el-button
+              type="primary"
+              size="small"
+              @click="addSubGroup"
+              :disabled="(trigger.conditionGroups?.length || 0) >= maxSubGroups"
+            >
+              <Icon icon="ep:plus" />
+              添加子条件组
+            </el-button>
+            <el-button type="danger" size="small" text @click="removeConditionGroup">
+              <Icon icon="ep:delete" />
+              删除条件组
+            </el-button>
+          </div>
+        </div>
+
+        <!-- 子条件组列表 -->
+        <div
+          v-if="trigger.conditionGroups && trigger.conditionGroups.length > 0"
+          class="space-y-16px"
+        >
+          <!-- 逻辑关系说明 -->
+          <div class="relative">
+            <div
+              v-for="(subGroup, subGroupIndex) in trigger.conditionGroups"
+              :key="`sub-group-${subGroupIndex}`"
+              class="relative"
+            >
+              <!-- 子条件组容器 -->
+              <div
+                class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow"
+              >
+                <div
+                  class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px"
+                >
+                  <div class="flex items-center gap-12px">
+                    <div class="flex items-center gap-8px text-16px font-600 text-orange-700">
+                      <div
+                        class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+                      >
+                        {{ subGroupIndex + 1 }}
+                      </div>
+                      <span>子条件组 {{ subGroupIndex + 1 }}</span>
+                    </div>
+                    <el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag>
+                    <el-tag size="small" type="info"> {{ subGroup?.length || 0 }}个条件 </el-tag>
+                  </div>
+                  <el-button
+                    type="danger"
+                    size="small"
+                    text
+                    @click="removeSubGroup(subGroupIndex)"
+                    class="hover:bg-red-50"
+                  >
+                    <Icon icon="ep:delete" />
+                    删除组
+                  </el-button>
+                </div>
+
+                <SubConditionGroupConfig
+                  :model-value="subGroup"
+                  @update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
+                  :trigger-type="trigger.type"
+                  :max-conditions="maxConditionsPerGroup"
+                />
+              </div>
+
+              <!-- 子条件组间的"或"连接符 -->
+              <div
+                v-if="subGroupIndex < trigger.conditionGroups!.length - 1"
+                class="flex items-center justify-center py-12px"
+              >
+                <div class="flex items-center gap-8px">
+                  <!-- 连接线 -->
+                  <div class="w-32px h-1px bg-orange-300"></div>
+                  <!-- 或标签 -->
+                  <div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
+                    <span class="text-14px font-600 text-orange-600">或</span>
+                  </div>
+                  <!-- 连接线 -->
+                  <div class="w-32px h-1px bg-orange-300"></div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 空状态 -->
+        <div
+          v-else
+          class="p-24px border-2 border-dashed border-orange-200 rounded-8px text-center bg-orange-50"
+        >
+          <div class="flex flex-col items-center gap-12px">
+            <Icon icon="ep:plus" class="text-32px text-orange-400" />
+            <div class="text-orange-600">
+              <p class="text-14px font-500 mb-4px">暂无子条件组</p>
+              <p class="text-12px">点击上方"添加子条件组"按钮开始配置</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+
+import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
+import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
+import type { Trigger } from '@/api/iot/rule/scene'
+
+/** 设备触发配置组件 */
+defineOptions({ name: 'DeviceTriggerConfig' })
+
+const props = defineProps<{
+  modelValue: Trigger
+  index: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: Trigger): void
+  (e: 'trigger-type-change', type: number): void
+}>()
+
+const trigger = useVModel(props, 'modelValue', emit)
+
+const maxSubGroups = 3 // 最多 3 个子条件组
+const maxConditionsPerGroup = 3 // 每组最多 3 个条件
+
+/**
+ * 更新条件
+ * @param condition 条件对象
+ */
+const updateCondition = (condition: Trigger) => {
+  trigger.value = condition
+}
+
+/**
+ * 处理触发器类型变化事件
+ * @param type 触发器类型
+ */
+const handleTriggerTypeChange = (type: number) => {
+  trigger.value.type = type
+  emit('trigger-type-change', type)
+}
+
+/** 添加子条件组 */
+const addSubGroup = async () => {
+  if (!trigger.value.conditionGroups) {
+    trigger.value.conditionGroups = []
+  }
+
+  // 检查是否达到最大子组数量限制
+  if (trigger.value.conditionGroups?.length >= maxSubGroups) {
+    return
+  }
+
+  // 使用 nextTick 确保响应式更新完成后再添加新的子组
+  await nextTick()
+  if (trigger.value.conditionGroups) {
+    trigger.value.conditionGroups.push([])
+  }
+}
+
+/**
+ * 移除子条件组
+ * @param index 子条件组索引
+ */
+const removeSubGroup = (index: number) => {
+  if (trigger.value.conditionGroups) {
+    trigger.value.conditionGroups.splice(index, 1)
+  }
+}
+
+/**
+ * 更新子条件组
+ * @param index 子条件组索引
+ * @param subGroup 子条件组数据
+ */
+const updateSubGroup = (index: number, subGroup: any) => {
+  if (trigger.value.conditionGroups) {
+    trigger.value.conditionGroups[index] = subGroup
+  }
+}
+
+/** 移除整个条件组 */
+const removeConditionGroup = () => {
+  trigger.value.conditionGroups = undefined
+}
+</script>

+ 340 - 0
src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue

@@ -0,0 +1,340 @@
+<template>
+  <div class="space-y-16px">
+    <!-- 触发事件类型选择 -->
+    <el-form-item label="触发事件类型" required>
+      <el-select
+        :model-value="triggerType"
+        @update:model-value="handleTriggerTypeChange"
+        placeholder="请选择触发事件类型"
+        class="w-full"
+      >
+        <el-option
+          v-for="option in triggerTypeOptions"
+          :key="option.value"
+          :label="option.label"
+          :value="option.value"
+        />
+      </el-select>
+    </el-form-item>
+
+    <!-- 设备属性条件配置 -->
+    <div v-if="isDevicePropertyTrigger" class="space-y-16px">
+      <!-- 产品设备选择 -->
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <el-form-item label="产品" required>
+            <ProductSelector
+              :model-value="condition.productId"
+              @update:model-value="(value) => updateConditionField('productId', value)"
+              @change="handleProductChange"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="设备" required>
+            <DeviceSelector
+              :model-value="condition.deviceId"
+              @update:model-value="(value) => updateConditionField('deviceId', value)"
+              :product-id="condition.productId"
+              @change="handleDeviceChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 属性配置 -->
+      <el-row :gutter="16">
+        <!-- 属性/事件/服务选择 -->
+        <el-col :span="6">
+          <el-form-item label="监控项" required>
+            <PropertySelector
+              :model-value="condition.identifier"
+              @update:model-value="(value) => updateConditionField('identifier', value)"
+              :trigger-type="triggerType"
+              :product-id="condition.productId"
+              :device-id="condition.deviceId"
+              @change="handlePropertyChange"
+            />
+          </el-form-item>
+        </el-col>
+
+        <!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
+        <el-col v-if="needsOperatorSelector" :span="6">
+          <el-form-item label="操作符" required>
+            <OperatorSelector
+              :model-value="condition.operator"
+              @update:model-value="(value) => updateConditionField('operator', value)"
+              :property-type="propertyType"
+            />
+          </el-form-item>
+        </el-col>
+
+        <!-- 值输入 -->
+        <el-col :span="isWideValueColumn ? 18 : 12">
+          <el-form-item :label="valueInputLabel" required>
+            <!-- 服务调用参数配置 -->
+            <JsonParamsInput
+              v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
+              v-model="condition.value"
+              type="service"
+              :config="serviceConfig"
+              placeholder="请输入 JSON 格式的服务参数"
+            />
+            <!-- 事件上报参数配置 -->
+            <JsonParamsInput
+              v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
+              v-model="condition.value"
+              type="event"
+              :config="eventConfig"
+              placeholder="请输入 JSON 格式的事件参数"
+            />
+            <!-- 普通值输入 -->
+            <ValueInput
+              v-else
+              :model-value="condition.value"
+              @update:model-value="(value) => updateConditionField('value', value)"
+              :property-type="propertyType"
+              :operator="condition.operator"
+              :property-config="propertyConfig"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 设备状态条件配置 -->
+    <div v-else-if="isDeviceStatusTrigger" class="space-y-16px">
+      <!-- 设备状态触发器使用简化的配置 -->
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <el-form-item label="产品" required>
+            <ProductSelector
+              :model-value="condition.productId"
+              @update:model-value="(value) => updateConditionField('productId', value)"
+              @change="handleProductChange"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="设备" required>
+            <DeviceSelector
+              :model-value="condition.deviceId"
+              @update:model-value="(value) => updateConditionField('deviceId', value)"
+              :product-id="condition.productId"
+              @change="handleDeviceChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="16">
+        <el-col :span="6">
+          <el-form-item label="操作符" required>
+            <el-select
+              :model-value="condition.operator"
+              @update:model-value="(value) => updateConditionField('operator', value)"
+              placeholder="请选择操作符"
+              class="w-full"
+            >
+              <el-option
+                :label="IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name"
+                :value="IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="参数" required>
+            <el-select
+              :model-value="condition.value"
+              @update:model-value="(value) => updateConditionField('value', value)"
+              placeholder="请选择操作符"
+              class="w-full"
+            >
+              <el-option
+                v-for="option in deviceStatusChangeOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 其他触发类型的提示 -->
+    <div v-else class="text-center py-20px">
+      <p class="text-14px text-[var(--el-text-color-secondary)] mb-4px">
+        当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
+      </p>
+      <p class="text-12px text-[var(--el-text-color-placeholder)]">
+        此触发类型暂不需要配置额外条件
+      </p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import PropertySelector from '../selectors/PropertySelector.vue'
+import OperatorSelector from '../selectors/OperatorSelector.vue'
+import ValueInput from '../inputs/ValueInput.vue'
+import JsonParamsInput from '../inputs/JsonParamsInput.vue'
+
+import type { Trigger } from '@/api/iot/rule/scene'
+import {
+  IotRuleSceneTriggerTypeEnum,
+  triggerTypeOptions,
+  getTriggerTypeLabel,
+  IotRuleSceneTriggerConditionParameterOperatorEnum,
+  IoTDeviceStatusEnum
+} from '@/views/iot/utils/constants'
+import { useVModel } from '@vueuse/core'
+
+/** 主条件内部配置组件 */
+defineOptions({ name: 'MainConditionInnerConfig' })
+
+const props = defineProps<{
+  modelValue: Trigger
+  triggerType: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: Trigger): void
+  (e: 'trigger-type-change', value: number): void
+}>()
+
+/** 获取设备状态变更选项(用于触发器配置) */
+const deviceStatusChangeOptions = [
+  {
+    label: IoTDeviceStatusEnum.ONLINE.label,
+    value: IoTDeviceStatusEnum.ONLINE.value
+  },
+  {
+    label: IoTDeviceStatusEnum.OFFLINE.label,
+    value: IoTDeviceStatusEnum.OFFLINE.value
+  }
+]
+
+const condition = useVModel(props, 'modelValue', emit)
+const propertyType = ref('') // 属性类型
+const propertyConfig = ref<any>(null) // 属性配置
+
+// 计算属性:是否为设备属性触发器
+const isDevicePropertyTrigger = computed(() => {
+  return (
+    props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
+    props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
+    props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  )
+})
+
+// 计算属性:是否为设备状态触发器
+const isDeviceStatusTrigger = computed(() => {
+  return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
+})
+
+// 计算属性:是否需要操作符选择(服务调用和事件上报不需要操作符)
+const needsOperatorSelector = computed(() => {
+  const noOperatorTriggerTypes = [
+    IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+    IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+  ] as number[]
+  return !noOperatorTriggerTypes.includes(props.triggerType)
+})
+
+// 计算属性:是否需要宽列布局(服务调用和事件上报不需要操作符列,所以值输入列更宽)
+const isWideValueColumn = computed(() => {
+  const wideColumnTriggerTypes = [
+    IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+    IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+  ] as number[]
+  return wideColumnTriggerTypes.includes(props.triggerType)
+})
+
+// 计算属性:值输入字段的标签文本
+const valueInputLabel = computed(() => {
+  return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+    ? '服务参数'
+    : '比较值'
+})
+
+// 计算属性:服务配置 - 用于 JsonParamsInput
+const serviceConfig = computed(() => {
+  if (
+    propertyConfig.value &&
+    props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  ) {
+    return {
+      service: {
+        name: propertyConfig.value.name || '服务',
+        inputParams: propertyConfig.value.inputParams || []
+      }
+    }
+  }
+  return undefined
+})
+
+// 计算属性:事件配置 - 用于 JsonParamsInput
+const eventConfig = computed(() => {
+  if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
+    return {
+      event: {
+        name: propertyConfig.value.name || '事件',
+        outputParams: propertyConfig.value.outputParams || []
+      }
+    }
+  }
+  return undefined
+})
+
+/**
+ * 更新条件字段
+ * @param field 字段名
+ * @param value 字段值
+ */
+const updateConditionField = (field: any, value: any) => {
+  condition.value[field] = value
+}
+
+/**
+ * 处理触发器类型变化事件
+ * @param type 触发器类型
+ */
+const handleTriggerTypeChange = (type: number) => {
+  emit('trigger-type-change', type)
+}
+
+/** 处理产品变化事件 */
+const handleProductChange = () => {
+  // 产品变化时清空设备和属性
+  condition.value.deviceId = undefined
+  condition.value.identifier = ''
+}
+
+/** 处理设备变化事件 */
+const handleDeviceChange = () => {
+  // 设备变化时清空属性
+  condition.value.identifier = ''
+}
+
+/**
+ * 处理属性变化事件
+ * @param propertyInfo 属性信息对象
+ */
+const handlePropertyChange = (propertyInfo: any) => {
+  if (propertyInfo) {
+    propertyType.value = propertyInfo.type
+    propertyConfig.value = propertyInfo.config
+
+    // 对于事件上报和服务调用,自动设置操作符为 '='
+    if (
+      props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
+      props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+    ) {
+      condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
+    }
+  }
+}
+</script>

+ 156 - 0
src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="p-16px">
+    <!-- 空状态 -->
+    <div v-if="!subGroup || subGroup.length === 0" class="text-center py-24px">
+      <div class="flex flex-col items-center gap-12px">
+        <Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
+        <div class="text-[var(--el-text-color-secondary)]">
+          <p class="text-14px font-500 mb-4px">暂无条件</p>
+          <p class="text-12px">点击下方按钮添加第一个条件</p>
+        </div>
+        <el-button type="primary" @click="addCondition">
+          <Icon icon="ep:plus" />
+          添加条件
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 条件列表 -->
+    <div v-else class="space-y-16px">
+      <div
+        v-for="(condition, conditionIndex) in subGroup"
+        :key="`condition-${conditionIndex}`"
+        class="relative"
+      >
+        <!-- 条件配置 -->
+        <div
+          class="border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)] shadow-sm"
+        >
+          <div
+            class="flex items-center justify-between p-12px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)] rounded-t-4px"
+          >
+            <div class="flex items-center gap-8px">
+              <div
+                class="w-20px h-20px bg-blue-500 text-white rounded-full flex items-center justify-center text-10px font-bold"
+              >
+                {{ conditionIndex + 1 }}
+              </div>
+              <span class="text-12px font-500 text-[var(--el-text-color-primary)]"
+                >条件 {{ conditionIndex + 1 }}</span
+              >
+            </div>
+            <el-button
+              type="danger"
+              size="small"
+              text
+              @click="removeCondition(conditionIndex)"
+              v-if="subGroup!.length > 1"
+              class="hover:bg-red-50"
+            >
+              <Icon icon="ep:delete" />
+            </el-button>
+          </div>
+
+          <div class="p-12px">
+            <ConditionConfig
+              :model-value="condition"
+              @update:model-value="(value) => updateCondition(conditionIndex, value)"
+              :trigger-type="triggerType"
+            />
+          </div>
+        </div>
+      </div>
+
+      <!-- 添加条件按钮 -->
+      <div
+        v-if="subGroup && subGroup.length > 0 && subGroup.length < maxConditions"
+        class="text-center py-16px"
+      >
+        <el-button type="primary" plain @click="addCondition">
+          <Icon icon="ep:plus" />
+          继续添加条件
+        </el-button>
+        <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
+          最多可添加 {{ maxConditions }} 个条件
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { nextTick } from 'vue'
+import { useVModel } from '@vueuse/core'
+import ConditionConfig from './ConditionConfig.vue'
+import type { TriggerCondition } from '@/api/iot/rule/scene'
+import {
+  IotRuleSceneTriggerConditionTypeEnum,
+  IotRuleSceneTriggerConditionParameterOperatorEnum
+} from '@/views/iot/utils/constants'
+
+/** 子条件组配置组件 */
+defineOptions({ name: 'SubConditionGroupConfig' })
+
+const props = defineProps<{
+  modelValue: TriggerCondition[]
+  triggerType: number
+  maxConditions?: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: TriggerCondition[]): void
+}>()
+
+const subGroup = useVModel(props, 'modelValue', emit)
+
+const maxConditions = computed(() => props.maxConditions || 3) // 最大条件数量
+
+/** 添加条件 */
+const addCondition = async () => {
+  // 确保 subGroup.value 是一个数组
+  if (!subGroup.value) {
+    subGroup.value = []
+  }
+
+  // 检查是否达到最大条件数量限制
+  if (subGroup.value?.length >= maxConditions.value) {
+    return
+  }
+
+  const newCondition: TriggerCondition = {
+    type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
+    productId: undefined,
+    deviceId: undefined,
+    identifier: '',
+    operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使用枚举默认值
+    param: ''
+  }
+
+  // 使用 nextTick 确保响应式更新完成后再添加新条件
+  await nextTick()
+  if (subGroup.value) {
+    subGroup.value.push(newCondition)
+  }
+}
+
+/**
+ * 移除条件
+ * @param index 条件索引
+ */
+const removeCondition = (index: number) => {
+  if (subGroup.value) {
+    subGroup.value.splice(index, 1)
+  }
+}
+
+/**
+ * 更新条件
+ * @param index 条件索引
+ * @param condition 条件对象
+ */
+const updateCondition = (index: number, condition: TriggerCondition) => {
+  if (subGroup.value) {
+    subGroup.value[index] = condition
+  }
+}
+</script>

+ 519 - 0
src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue

@@ -0,0 +1,519 @@
+<!-- JSON参数输入组件 - 通用版本 -->
+<template>
+  <!-- 参数配置 -->
+  <div class="w-full space-y-12px">
+    <!-- JSON 输入框 -->
+    <div class="relative">
+      <el-input
+        v-model="paramsJson"
+        type="textarea"
+        :rows="4"
+        :placeholder="placeholder"
+        @input="handleParamsChange"
+        :class="{ 'is-error': jsonError }"
+      />
+      <!-- 查看详细示例弹出层 -->
+      <div class="absolute top-8px right-8px">
+        <el-popover
+          placement="left-start"
+          :width="450"
+          trigger="click"
+          :show-arrow="true"
+          :offset="8"
+          popper-class="json-params-detail-popover"
+        >
+          <template #reference>
+            <el-button
+              type="info"
+              :icon="InfoFilled"
+              circle
+              size="small"
+              :title="JSON_PARAMS_INPUT_CONSTANTS.VIEW_EXAMPLE_TITLE"
+            />
+          </template>
+
+          <!-- 弹出层内容 -->
+          <div class="json-params-detail-content">
+            <div class="flex items-center gap-8px mb-16px">
+              <Icon :icon="titleIcon" class="text-[var(--el-color-primary)] text-18px" />
+              <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
+                {{ title }}
+              </span>
+            </div>
+
+            <div class="space-y-16px">
+              <!-- 参数列表 -->
+              <div v-if="paramsList.length > 0">
+                <div class="flex items-center gap-8px mb-8px">
+                  <Icon :icon="paramsIcon" class="text-[var(--el-color-primary)] text-14px" />
+                  <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                    {{ paramsLabel }}
+                  </span>
+                </div>
+                <div class="ml-22px space-y-8px">
+                  <div
+                    v-for="param in paramsList"
+                    :key="param.identifier"
+                    class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
+                  >
+                    <div class="flex-1">
+                      <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
+                        {{ param.name }}
+                        <el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
+                          {{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
+                        </el-tag>
+                      </div>
+                      <div class="text-11px text-[var(--el-text-color-secondary)]">
+                        {{ param.identifier }}
+                      </div>
+                    </div>
+                    <div class="flex items-center gap-8px">
+                      <el-tag :type="getParamTypeTag(param.dataType)" size="small">
+                        {{ getParamTypeName(param.dataType) }}
+                      </el-tag>
+                      <span class="text-11px text-[var(--el-text-color-secondary)]">
+                        {{ getExampleValue(param) }}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+
+                <div class="mt-12px ml-22px">
+                  <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
+                    {{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
+                  </div>
+                  <pre
+                    class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
+                  >
+                      <code>{{ generateExampleJson() }}</code>
+                    </pre>
+                </div>
+              </div>
+
+              <!-- 无参数提示 -->
+              <div v-else>
+                <div class="text-center py-16px">
+                  <p class="text-14px text-[var(--el-text-color-secondary)]">{{ emptyMessage }}</p>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-popover>
+      </div>
+    </div>
+
+    <!-- 验证状态和错误提示 -->
+    <div class="flex items-center justify-between">
+      <div class="flex items-center gap-8px">
+        <Icon
+          :icon="
+            jsonError
+              ? JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.ERROR
+              : JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.SUCCESS
+          "
+          :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
+          class="text-14px"
+        />
+        <span
+          :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
+          class="text-12px"
+        >
+          {{ jsonError || JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_CORRECT }}
+        </span>
+      </div>
+
+      <!-- 快速填充按钮 -->
+      <div v-if="paramsList.length > 0" class="flex items-center gap-8px">
+        <span class="text-12px text-[var(--el-text-color-secondary)]">{{
+          JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL
+        }}</span>
+        <el-button size="small" type="primary" plain @click="fillExampleJson">
+          {{ JSON_PARAMS_INPUT_CONSTANTS.EXAMPLE_DATA_BUTTON }}
+        </el-button>
+        <el-button size="small" type="danger" plain @click="clearParams">{{
+          JSON_PARAMS_INPUT_CONSTANTS.CLEAR_BUTTON
+        }}</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { InfoFilled } from '@element-plus/icons-vue'
+import {
+  IoTDataSpecsDataTypeEnum,
+  JSON_PARAMS_INPUT_CONSTANTS,
+  JSON_PARAMS_INPUT_ICONS,
+  JSON_PARAMS_EXAMPLE_VALUES,
+  JsonParamsInputTypeEnum,
+  type JsonParamsInputType
+} from '@/views/iot/utils/constants'
+
+/** JSON参数输入组件 - 通用版本 */
+defineOptions({ name: 'JsonParamsInput' })
+
+interface JsonParamsConfig {
+  // 服务配置
+  service?: {
+    name: string
+    inputParams?: any[]
+  }
+  // 事件配置
+  event?: {
+    name: string
+    outputParams?: any[]
+  }
+  // 属性配置
+  properties?: any[]
+  // 自定义配置
+  custom?: {
+    name: string
+    params: any[]
+  }
+}
+
+interface Props {
+  modelValue?: string
+  config?: JsonParamsConfig
+  type?: JsonParamsInputType
+  placeholder?: string
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: string): void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  type: JsonParamsInputTypeEnum.SERVICE,
+  placeholder: JSON_PARAMS_INPUT_CONSTANTS.PLACEHOLDER
+})
+
+const emit = defineEmits<Emits>()
+
+const localValue = useVModel(props, 'modelValue', emit, {
+  defaultValue: ''
+})
+
+const paramsJson = ref('') // JSON参数字符串
+const jsonError = ref('') // JSON验证错误信息
+
+// 计算属性:参数列表
+const paramsList = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return props.config?.service?.inputParams || []
+    case JsonParamsInputTypeEnum.EVENT:
+      return props.config?.event?.outputParams || []
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return props.config?.properties || []
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return props.config?.custom?.params || []
+    default:
+      return []
+  }
+})
+
+// 计算属性:标题
+const title = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return JSON_PARAMS_INPUT_CONSTANTS.TITLES.SERVICE(props.config?.service?.name)
+    case JsonParamsInputTypeEnum.EVENT:
+      return JSON_PARAMS_INPUT_CONSTANTS.TITLES.EVENT(props.config?.event?.name)
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return JSON_PARAMS_INPUT_CONSTANTS.TITLES.PROPERTY
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return JSON_PARAMS_INPUT_CONSTANTS.TITLES.CUSTOM(props.config?.custom?.name)
+    default:
+      return JSON_PARAMS_INPUT_CONSTANTS.TITLES.DEFAULT
+  }
+})
+
+// 计算属性:标题图标
+const titleIcon = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.SERVICE
+    case JsonParamsInputTypeEnum.EVENT:
+      return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.EVENT
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.PROPERTY
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.CUSTOM
+    default:
+      return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.DEFAULT
+  }
+})
+
+// 计算属性:参数图标
+const paramsIcon = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.SERVICE
+    case JsonParamsInputTypeEnum.EVENT:
+      return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.EVENT
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.PROPERTY
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.CUSTOM
+    default:
+      return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.DEFAULT
+  }
+})
+
+// 计算属性:参数标签
+const paramsLabel = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.SERVICE
+    case JsonParamsInputTypeEnum.EVENT:
+      return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.EVENT
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.PROPERTY
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.CUSTOM
+    default:
+      return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.DEFAULT
+  }
+})
+
+// 计算属性:空状态消息
+const emptyMessage = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.SERVICE
+    case JsonParamsInputTypeEnum.EVENT:
+      return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.EVENT
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.PROPERTY
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.CUSTOM
+    default:
+      return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.DEFAULT
+  }
+})
+
+// 计算属性:无配置消息
+const noConfigMessage = computed(() => {
+  switch (props.type) {
+    case JsonParamsInputTypeEnum.SERVICE:
+      return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.SERVICE
+    case JsonParamsInputTypeEnum.EVENT:
+      return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.EVENT
+    case JsonParamsInputTypeEnum.PROPERTY:
+      return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.PROPERTY
+    case JsonParamsInputTypeEnum.CUSTOM:
+      return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.CUSTOM
+    default:
+      return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.DEFAULT
+  }
+})
+
+/**
+ * 处理参数变化事件
+ */
+const handleParamsChange = () => {
+  try {
+    jsonError.value = '' // 清除之前的错误
+
+    if (paramsJson.value.trim()) {
+      const parsed = JSON.parse(paramsJson.value)
+      localValue.value = paramsJson.value
+
+      // 额外的参数验证
+      if (typeof parsed !== 'object' || parsed === null) {
+        jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT
+        return
+      }
+
+      // 验证必填参数
+      for (const param of paramsList.value) {
+        if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
+          jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(param.name)
+          return
+        }
+      }
+    } else {
+      localValue.value = ''
+    }
+
+    // 验证通过
+    jsonError.value = ''
+  } catch (error) {
+    jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
+      error instanceof Error ? error.message : JSON_PARAMS_INPUT_CONSTANTS.UNKNOWN_ERROR
+    )
+  }
+}
+
+/**
+ * 快速填充示例数据
+ */
+const fillExampleJson = () => {
+  paramsJson.value = generateExampleJson()
+  handleParamsChange()
+}
+
+/**
+ * 清空参数
+ */
+const clearParams = () => {
+  paramsJson.value = ''
+  localValue.value = ''
+  jsonError.value = ''
+}
+
+/**
+ * 获取参数类型名称
+ * @param dataType 数据类型
+ * @returns 类型名称
+ */
+const getParamTypeName = (dataType: string) => {
+  // 使用 constants.ts 中已有的 getDataTypeName 函数逻辑
+  const typeMap = {
+    [IoTDataSpecsDataTypeEnum.INT]: '整数',
+    [IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
+    [IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
+    [IoTDataSpecsDataTypeEnum.TEXT]: '字符串',
+    [IoTDataSpecsDataTypeEnum.BOOL]: '布尔值',
+    [IoTDataSpecsDataTypeEnum.ENUM]: '枚举',
+    [IoTDataSpecsDataTypeEnum.DATE]: '日期',
+    [IoTDataSpecsDataTypeEnum.STRUCT]: '结构体',
+    [IoTDataSpecsDataTypeEnum.ARRAY]: '数组'
+  }
+  return typeMap[dataType] || dataType
+}
+
+/**
+ * 获取参数类型标签样式
+ * @param dataType 数据类型
+ * @returns 标签样式
+ */
+const getParamTypeTag = (dataType: string) => {
+  const tagMap = {
+    [IoTDataSpecsDataTypeEnum.INT]: 'primary',
+    [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
+    [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
+    [IoTDataSpecsDataTypeEnum.TEXT]: 'info',
+    [IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
+    [IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
+    [IoTDataSpecsDataTypeEnum.DATE]: 'primary',
+    [IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
+    [IoTDataSpecsDataTypeEnum.ARRAY]: 'warning'
+  }
+  return tagMap[dataType] || 'info'
+}
+
+/**
+ * 获取示例值
+ * @param param 参数对象
+ * @returns 示例值
+ */
+const getExampleValue = (param: any) => {
+  const exampleConfig =
+    JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
+  return exampleConfig.display
+}
+
+/**
+ * 生成示例JSON
+ * @returns JSON字符串
+ */
+const generateExampleJson = () => {
+  if (paramsList.value.length === 0) {
+    return '{}'
+  }
+
+  const example = {}
+  paramsList.value.forEach((param) => {
+    const exampleConfig =
+      JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
+    example[param.identifier] = exampleConfig.value
+  })
+
+  return JSON.stringify(example, null, 2)
+}
+
+/**
+ * 处理数据回显
+ * @param value 值字符串
+ */
+const handleDataDisplay = (value: string) => {
+  if (!value || !value.trim()) {
+    paramsJson.value = ''
+    jsonError.value = ''
+    return
+  }
+
+  try {
+    // 尝试解析JSON,如果成功则格式化
+    const parsed = JSON.parse(value)
+    paramsJson.value = JSON.stringify(parsed, null, 2)
+    jsonError.value = ''
+  } catch {
+    // 如果不是有效的JSON,直接使用原字符串
+    paramsJson.value = value
+    jsonError.value = ''
+  }
+}
+
+// 监听外部值变化(编辑模式数据回显)
+watch(
+  () => localValue.value,
+  async (newValue, oldValue) => {
+    // 避免循环更新
+    if (newValue === oldValue) return
+
+    // 使用 nextTick 确保在下一个 tick 中处理数据
+    await nextTick()
+    handleDataDisplay(newValue || '')
+  },
+  { immediate: true }
+)
+
+// 组件挂载后也尝试处理一次数据回显
+onMounted(async () => {
+  await nextTick()
+  if (localValue.value) {
+    handleDataDisplay(localValue.value)
+  }
+})
+
+// 监听配置变化
+watch(
+  () => props.config,
+  (newConfig, oldConfig) => {
+    // 只有在配置真正变化时才清空数据
+    if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
+      // 如果没有外部传入的值,才清空数据
+      if (!localValue.value) {
+        paramsJson.value = ''
+        jsonError.value = ''
+      }
+    }
+  }
+)
+</script>
+
+<style scoped>
+/* 弹出层内容样式 */
+.json-params-detail-content {
+  padding: 4px 0;
+}
+
+/* 弹出层自定义样式 */
+:global(.json-params-detail-popover) {
+  max-width: 500px !important;
+}
+
+:global(.json-params-detail-popover .el-popover__content) {
+  padding: 16px !important;
+}
+
+/* JSON 代码块样式 */
+.json-params-detail-content pre {
+  max-height: 200px;
+  overflow-y: auto;
+}
+</style>

+ 310 - 0
src/views/iot/rule/scene/form/inputs/ValueInput.vue

@@ -0,0 +1,310 @@
+<!-- 值输入组件 -->
+<!-- TODO @yunai:这个需要在看看。。。 -->
+<template>
+  <div class="w-full min-w-0">
+    <!-- 布尔值选择 -->
+    <el-select
+      v-if="propertyType === 'bool'"
+      v-model="localValue"
+      placeholder="请选择布尔值"
+      @change="handleChange"
+      class="w-full!"
+      style="width: 100% !important"
+    >
+      <el-option label="真 (true)" value="true" />
+      <el-option label="假 (false)" value="false" />
+    </el-select>
+
+    <!-- 枚举值选择 -->
+    <el-select
+      v-else-if="propertyType === 'enum' && enumOptions.length > 0"
+      v-model="localValue"
+      placeholder="请选择枚举值"
+      @change="handleChange"
+      class="w-full!"
+      style="width: 100% !important"
+    >
+      <el-option
+        v-for="option in enumOptions"
+        :key="option.value"
+        :label="option.label"
+        :value="option.value"
+      />
+    </el-select>
+
+    <!-- 范围输入 (between 操作符) -->
+    <div
+      v-else-if="operator === 'between'"
+      class="w-full! flex items-center gap-8px"
+      style="width: 100% !important"
+    >
+      <el-input
+        v-model="rangeStart"
+        :type="getInputType()"
+        placeholder="最小值"
+        @input="handleRangeChange"
+        class="flex-1 min-w-0"
+        style="width: auto !important"
+      />
+      <span class="text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap">至</span>
+      <el-input
+        v-model="rangeEnd"
+        :type="getInputType()"
+        placeholder="最大值"
+        @input="handleRangeChange"
+        class="flex-1 min-w-0"
+        style="width: auto !important"
+      />
+    </div>
+
+    <!-- 列表输入 (in 操作符) -->
+    <div v-else-if="operator === 'in'" class="w-full!" style="width: 100% !important">
+      <el-input
+        v-model="localValue"
+        placeholder="请输入值列表,用逗号分隔"
+        @input="handleChange"
+        class="w-full!"
+        style="width: 100% !important"
+      >
+        <template #suffix>
+          <el-tooltip content="多个值用逗号分隔,如:1,2,3" placement="top">
+            <Icon
+              icon="ep:question-filled"
+              class="text-[var(--el-text-color-placeholder)] cursor-help"
+            />
+          </el-tooltip>
+        </template>
+      </el-input>
+      <div v-if="listPreview.length > 0" class="mt-8px flex items-center gap-6px flex-wrap">
+        <span class="text-12px text-[var(--el-text-color-secondary)]">解析结果:</span>
+        <el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="m-0">
+          {{ item }}
+        </el-tag>
+      </div>
+    </div>
+
+    <!-- 日期时间输入 -->
+    <el-date-picker
+      v-else-if="propertyType === 'date'"
+      v-model="dateValue"
+      type="datetime"
+      placeholder="请选择日期时间"
+      format="YYYY-MM-DD HH:mm:ss"
+      value-format="YYYY-MM-DD HH:mm:ss"
+      @change="handleDateChange"
+      class="w-full!"
+      style="width: 100% !important"
+    />
+
+    <!-- 数字输入 -->
+    <el-input-number
+      v-else-if="isNumericType()"
+      v-model="numberValue"
+      :precision="getPrecision()"
+      :step="getStep()"
+      :min="getMin()"
+      :max="getMax()"
+      placeholder="请输入数值"
+      @change="handleNumberChange"
+      class="w-full!"
+      style="width: 100% !important"
+    />
+
+    <!-- 文本输入 -->
+    <el-input
+      v-else
+      v-model="localValue"
+      :type="getInputType()"
+      :placeholder="getPlaceholder()"
+      @input="handleChange"
+      class="w-full!"
+      style="width: 100% !important"
+    >
+      <template #suffix>
+        <el-tooltip
+          v-if="propertyConfig?.unit"
+          :content="`单位:${propertyConfig.unit}`"
+          placement="top"
+        >
+          <span class="text-12px text-[var(--el-text-color-secondary)] px-4px">{{
+            propertyConfig.unit
+          }}</span>
+        </el-tooltip>
+      </template>
+    </el-input>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+
+/** 值输入组件 */
+defineOptions({ name: 'ValueInput' })
+
+interface Props {
+  modelValue?: string
+  propertyType?: string
+  operator?: string
+  propertyConfig?: any
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: string): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+const localValue = useVModel(props, 'modelValue', emit, {
+  defaultValue: ''
+})
+
+const rangeStart = ref('') // 范围开始值
+const rangeEnd = ref('') // 范围结束值
+const dateValue = ref('') // 日期值
+const numberValue = ref<number>() // 数字值
+
+// 计算属性:枚举选项
+const enumOptions = computed(() => {
+  if (props.propertyConfig?.enum) {
+    return props.propertyConfig.enum.map((item: any) => ({
+      label: item.name || item.label || item.value,
+      value: item.value
+    }))
+  }
+  return []
+})
+
+// 计算属性:列表预览
+const listPreview = computed(() => {
+  if (props.operator === 'in' && localValue.value) {
+    return localValue.value
+      .split(',')
+      .map((item) => item.trim())
+      .filter((item) => item)
+  }
+  return []
+})
+
+/**
+ * 判断是否为数字类型
+ * @returns 是否为数字类型
+ */
+const isNumericType = () => {
+  return [
+    IoTDataSpecsDataTypeEnum.INT,
+    IoTDataSpecsDataTypeEnum.FLOAT,
+    IoTDataSpecsDataTypeEnum.DOUBLE
+  ].includes((props.propertyType || '') as any)
+}
+
+/**
+ * 获取输入框类型
+ * @returns 输入框类型
+ */
+const getInputType = () => {
+  switch (props.propertyType) {
+    case IoTDataSpecsDataTypeEnum.INT:
+    case IoTDataSpecsDataTypeEnum.FLOAT:
+    case IoTDataSpecsDataTypeEnum.DOUBLE:
+      return 'number'
+    default:
+      return 'text'
+  }
+}
+
+/**
+ * 获取占位符文本
+ * @returns 占位符文本
+ */
+const getPlaceholder = () => {
+  const typeMap = {
+    [IoTDataSpecsDataTypeEnum.TEXT]: '请输入字符串',
+    [IoTDataSpecsDataTypeEnum.INT]: '请输入整数',
+    [IoTDataSpecsDataTypeEnum.FLOAT]: '请输入浮点数',
+    [IoTDataSpecsDataTypeEnum.DOUBLE]: '请输入双精度数',
+    [IoTDataSpecsDataTypeEnum.STRUCT]: '请输入 JSON 格式数据',
+    [IoTDataSpecsDataTypeEnum.ARRAY]: '请输入数组格式数据'
+  }
+  return typeMap[props.propertyType || ''] || '请输入值'
+}
+
+/**
+ * 获取数字精度
+ * @returns 数字精度
+ */
+const getPrecision = () => {
+  return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 0 : 2
+}
+
+/**
+ * 获取数字步长
+ * @returns 数字步长
+ */
+const getStep = () => {
+  return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 1 : 0.1
+}
+
+/**
+ * 获取最小值
+ * @returns 最小值
+ */
+const getMin = () => {
+  return props.propertyConfig?.min || undefined
+}
+
+/**
+ * 获取最大值
+ * @returns 最大值
+ */
+const getMax = () => {
+  return props.propertyConfig?.max || undefined
+}
+
+/**
+ * 处理值变化事件
+ */
+const handleChange = () => {
+  // 值变化处理
+}
+
+/**
+ * 处理范围变化事件
+ */
+const handleRangeChange = () => {
+  if (rangeStart.value && rangeEnd.value) {
+    localValue.value = `${rangeStart.value},${rangeEnd.value}`
+  } else {
+    localValue.value = ''
+  }
+}
+
+/**
+ * 处理日期变化事件
+ * @param value 日期值
+ */
+const handleDateChange = (value: string) => {
+  localValue.value = value || ''
+}
+
+/**
+ * 处理数字变化事件
+ * @param value 数字值
+ */
+const handleNumberChange = (value: number | undefined) => {
+  localValue.value = value?.toString() || ''
+}
+
+// 监听操作符变化
+watch(
+  () => props.operator,
+  () => {
+    localValue.value = ''
+    rangeStart.value = ''
+    rangeEnd.value = ''
+    dateValue.value = ''
+    numberValue.value = undefined
+  }
+)
+</script>

+ 272 - 0
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -0,0 +1,272 @@
+<!-- 执行器配置组件 -->
+<template>
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
+    <template #header>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-8px">
+          <Icon icon="ep:setting" class="text-[var(--el-color-primary)] text-18px" />
+          <span class="text-16px font-600 text-[var(--el-text-color-primary)]">执行器配置</span>
+          <el-tag size="small" type="info">{{ actions.length }} 个执行器</el-tag>
+        </div>
+        <div class="flex items-center gap-8px">
+          <el-button type="primary" size="small" @click="addAction">
+            <Icon icon="ep:plus" />
+            添加执行器
+          </el-button>
+        </div>
+      </div>
+    </template>
+
+    <div class="p-0">
+      <!-- 空状态 -->
+      <div v-if="actions.length === 0">
+        <el-empty description="暂无执行器配置">
+          <el-button type="primary" @click="addAction">
+            <Icon icon="ep:plus" />
+            添加第一个执行器
+          </el-button>
+        </el-empty>
+      </div>
+
+      <!-- 执行器列表 -->
+      <div v-else class="space-y-24px">
+        <div
+          v-for="(action, index) in actions"
+          :key="`action-${index}`"
+          class="border-2 border-blue-200 rounded-8px bg-blue-50 shadow-sm hover:shadow-md transition-shadow"
+        >
+          <!-- 执行器头部 - 蓝色主题 -->
+          <div
+            class="flex items-center justify-between p-16px bg-gradient-to-r from-blue-50 to-sky-50 border-b border-blue-200 rounded-t-6px"
+          >
+            <div class="flex items-center gap-12px">
+              <div class="flex items-center gap-8px text-16px font-600 text-blue-700">
+                <div
+                  class="w-24px h-24px bg-blue-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+                >
+                  {{ index + 1 }}
+                </div>
+                <span>执行器 {{ index + 1 }}</span>
+              </div>
+              <el-tag :type="getActionTypeTag(action.type)" size="small" class="font-500">
+                {{ getActionTypeLabel(action.type) }}
+              </el-tag>
+            </div>
+            <div class="flex items-center gap-8px">
+              <el-button
+                v-if="actions.length > 1"
+                type="danger"
+                size="small"
+                text
+                @click="removeAction(index)"
+                class="hover:bg-red-50"
+              >
+                <Icon icon="ep:delete" />
+                删除
+              </el-button>
+            </div>
+          </div>
+
+          <!-- 执行器内容区域 -->
+          <div class="p-16px space-y-16px">
+            <!-- 执行类型选择 -->
+            <div class="w-full">
+              <el-form-item label="执行类型" required>
+                <el-select
+                  :model-value="action.type"
+                  @update:model-value="(value) => updateActionType(index, value)"
+                  @change="(value) => onActionTypeChange(action, value)"
+                  placeholder="请选择执行类型"
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="option in getActionTypeOptions()"
+                    :key="option.value"
+                    :label="option.label"
+                    :value="option.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </div>
+
+            <!-- 设备控制配置 -->
+            <DeviceControlConfig
+              v-if="isDeviceAction(action.type)"
+              :model-value="action"
+              @update:model-value="(value) => updateAction(index, value)"
+            />
+
+            <!-- 告警配置 - 只有恢复告警时才显示 -->
+            <AlertConfig
+              v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
+              :model-value="action.alertConfigId"
+              @update:model-value="(value) => updateActionAlertConfig(index, value)"
+            />
+
+            <!-- 触发告警提示 - 触发告警时显示 -->
+            <div
+              v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
+              class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
+            >
+              <div class="flex items-center gap-8px mb-8px">
+                <Icon icon="ep:warning" class="text-[var(--el-color-warning)] text-16px" />
+                <span class="text-14px font-600 text-[var(--el-text-color-primary)]">触发告警</span>
+                <el-tag size="small" type="warning">自动执行</el-tag>
+              </div>
+              <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
+                当触发条件满足时,系统将自动发送告警通知,无需额外配置。
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 添加提示 -->
+      <div v-if="actions.length > 0" class="text-center py-16px">
+        <el-button type="primary" plain @click="addAction">
+          <Icon icon="ep:plus" />
+          继续添加执行器
+        </el-button>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
+import AlertConfig from '../configs/AlertConfig.vue'
+import type { Action } from '@/api/iot/rule/scene'
+import {
+  getActionTypeLabel,
+  getActionTypeOptions,
+  IotRuleSceneActionTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** 执行器配置组件 */
+defineOptions({ name: 'ActionSection' })
+
+const props = defineProps<{
+  actions: Action[]
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:actions', value: Action[]): void
+}>()
+
+const actions = useVModel(props, 'actions', emit)
+
+/** 获取执行器标签类型(用于 el-tag 的 type 属性) */
+const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
+  const actionTypeTags = {
+    [IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
+    [IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
+    [IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
+    [IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning'
+  } as const
+  return actionTypeTags[type] || 'info'
+}
+
+/** 判断是否为设备执行器类型 */
+const isDeviceAction = (type: number): boolean => {
+  const deviceActionTypes = [
+    IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+    IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+  ] as number[]
+  return deviceActionTypes.includes(type)
+}
+
+/** 判断是否为告警执行器类型 */
+const isAlertAction = (type: number): boolean => {
+  const alertActionTypes = [
+    IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+    IotRuleSceneActionTypeEnum.ALERT_RECOVER
+  ] as number[]
+  return alertActionTypes.includes(type)
+}
+
+/**
+ * 创建默认的执行器数据
+ * @returns 默认执行器对象
+ */
+const createDefaultActionData = (): Action => {
+  return {
+    type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
+    productId: undefined,
+    deviceId: undefined,
+    identifier: undefined, // 物模型标识符(服务调用时使用)
+    params: undefined,
+    alertConfigId: undefined
+  }
+}
+
+/**
+ * 添加执行器
+ */
+const addAction = () => {
+  const newAction = createDefaultActionData()
+  actions.value.push(newAction)
+}
+
+/**
+ * 删除执行器
+ * @param index 执行器索引
+ */
+const removeAction = (index: number) => {
+  actions.value.splice(index, 1)
+}
+
+/**
+ * 更新执行器类型
+ * @param index 执行器索引
+ * @param type 执行器类型
+ */
+const updateActionType = (index: number, type: number) => {
+  actions.value[index].type = type
+  onActionTypeChange(actions.value[index], type)
+}
+
+/**
+ * 更新执行器
+ * @param index 执行器索引
+ * @param action 执行器对象
+ */
+const updateAction = (index: number, action: Action) => {
+  actions.value[index] = action
+}
+
+/**
+ * 更新告警配置
+ * @param index 执行器索引
+ * @param alertConfigId 告警配置ID
+ */
+const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
+  actions.value[index].alertConfigId = alertConfigId
+}
+
+/**
+ * 监听执行器类型变化
+ * @param action 执行器对象
+ * @param type 执行器类型
+ */
+const onActionTypeChange = (action: Action, type: number) => {
+  // 清理不相关的配置,确保数据结构干净
+  if (isDeviceAction(type)) {
+    // 设备控制类型:清理告警配置,确保设备参数存在
+    action.alertConfigId = undefined
+    if (!action.params) {
+      action.params = ''
+    }
+    // 如果从其他类型切换到设备控制类型,清空identifier(让用户重新选择)
+    if (action.identifier && type !== action.type) {
+      action.identifier = undefined
+    }
+  } else if (isAlertAction(type)) {
+    action.productId = undefined
+    action.deviceId = undefined
+    action.identifier = undefined // 清理服务标识符
+    action.params = undefined
+    action.alertConfigId = undefined
+  }
+}
+</script>

+ 86 - 0
src/views/iot/rule/scene/form/sections/BasicInfoSection.vue

@@ -0,0 +1,86 @@
+<!-- 基础信息配置组件 -->
+<template>
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
+    <template #header>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-8px">
+          <Icon icon="ep:info-filled" class="text-[var(--el-color-primary)] text-18px" />
+          <span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
+        </div>
+        <div class="flex items-center gap-8px">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
+        </div>
+      </div>
+    </template>
+
+    <div class="p-0">
+      <el-row :gutter="24" class="mb-24px">
+        <el-col :span="12">
+          <el-form-item label="场景名称" prop="name" required>
+            <el-input
+              v-model="formData.name"
+              placeholder="请输入场景名称"
+              maxlength="50"
+              show-word-limit
+              clearable
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="场景状态" prop="status" required>
+            <el-radio-group v-model="formData.status">
+              <el-radio
+                v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="场景描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          placeholder="请输入场景描述(可选)"
+          :rows="3"
+          maxlength="200"
+          show-word-limit
+          resize="none"
+        />
+      </el-form-item>
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import type { IotSceneRule } from '@/api/iot/rule/scene'
+
+/** 基础信息配置组件 */
+defineOptions({ name: 'BasicInfoSection' })
+
+const props = defineProps<{
+  modelValue: IotSceneRule
+  rules?: any
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: IotSceneRule): void
+}>()
+
+const formData = useVModel(props, 'modelValue', emit) // 表单数据
+</script>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 20px;
+}
+
+:deep(.el-form-item:last-child) {
+  margin-bottom: 0;
+}
+</style>

+ 222 - 0
src/views/iot/rule/scene/form/sections/TriggerSection.vue

@@ -0,0 +1,222 @@
+<template>
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
+    <template #header>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-8px">
+          <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
+          <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
+          <el-tag size="small" type="info">{{ triggers.length }} 个触发器</el-tag>
+        </div>
+        <el-button type="primary" size="small" @click="addTrigger">
+          <Icon icon="ep:plus" />
+          添加触发器
+        </el-button>
+      </div>
+    </template>
+
+    <div class="p-16px space-y-24px">
+      <!-- 触发器列表 -->
+      <div v-if="triggers.length > 0" class="space-y-24px">
+        <div
+          v-for="(triggerItem, index) in triggers"
+          :key="`trigger-${index}`"
+          class="border-2 border-green-200 rounded-8px bg-green-50 shadow-sm hover:shadow-md transition-shadow"
+        >
+          <!-- 触发器头部 - 绿色主题 -->
+          <div
+            class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border-b border-green-200 rounded-t-6px"
+          >
+            <div class="flex items-center gap-12px">
+              <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+                <div
+                  class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+                >
+                  {{ index + 1 }}
+                </div>
+                <span>触发器 {{ index + 1 }}</span>
+              </div>
+              <el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500">
+                {{ getTriggerTypeLabel(triggerItem.type) }}
+              </el-tag>
+            </div>
+            <div class="flex items-center gap-8px">
+              <el-button
+                v-if="triggers.length > 1"
+                type="danger"
+                size="small"
+                text
+                @click="removeTrigger(index)"
+                class="hover:bg-red-50"
+              >
+                <Icon icon="ep:delete" />
+                删除
+              </el-button>
+            </div>
+          </div>
+
+          <!-- 触发器内容区域 -->
+          <div class="p-16px space-y-16px">
+            <!-- 设备触发配置 -->
+            <DeviceTriggerConfig
+              v-if="isDeviceTrigger(triggerItem.type)"
+              :model-value="triggerItem"
+              :index="index"
+              @update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
+              @trigger-type-change="(type) => updateTriggerType(index, type)"
+            />
+
+            <!-- 定时触发配置 -->
+            <div
+              v-else-if="triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER"
+              class="flex flex-col gap-16px"
+            >
+              <div
+                class="flex items-center gap-8px p-12px px-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
+              >
+                <Icon icon="ep:timer" class="text-[var(--el-color-danger)] text-18px" />
+                <span class="text-14px font-500 text-[var(--el-text-color-primary)]"
+                  >定时触发配置</span
+                >
+              </div>
+
+              <!-- CRON 表达式配置 -->
+              <div
+                class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
+              >
+                <el-form-item label="CRON表达式" required>
+                  <Crontab
+                    :model-value="triggerItem.cronExpression || '0 0 12 * * ?'"
+                    @update:model-value="(value) => updateTriggerCronConfig(index, value)"
+                  />
+                </el-form-item>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="py-40px text-center">
+        <el-empty description="暂无触发器">
+          <template #description>
+            <div class="space-y-8px">
+              <p class="text-[var(--el-text-color-secondary)]">暂无触发器配置</p>
+              <p class="text-12px text-[var(--el-text-color-placeholder)]">
+                请使用上方的"添加触发器"按钮来设置触发规则
+              </p>
+            </div>
+          </template>
+        </el-empty>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
+import { Crontab } from '@/components/Crontab'
+import type { Trigger } from '@/api/iot/rule/scene'
+import {
+  getTriggerTypeLabel,
+  IotRuleSceneTriggerTypeEnum,
+  isDeviceTrigger
+} from '@/views/iot/utils/constants'
+
+/** 触发器配置组件 */
+defineOptions({ name: 'TriggerSection' })
+
+const props = defineProps<{
+  triggers: Trigger[]
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:triggers', value: Trigger[]): void
+}>()
+
+const triggers = useVModel(props, 'triggers', emit)
+
+/** 获取触发器标签类型(用于 el-tag 的 type 属性) */
+const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
+  if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
+    return 'warning'
+  }
+  return isDeviceTrigger(type) ? 'success' : 'info'
+}
+
+/** 添加触发器 */
+const addTrigger = () => {
+  const newTrigger: Trigger = {
+    type: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+    productId: undefined,
+    deviceId: undefined,
+    identifier: undefined,
+    operator: undefined,
+    value: undefined,
+    cronExpression: undefined,
+    conditionGroups: [] // 空的条件组数组
+  }
+  triggers.value.push(newTrigger)
+}
+
+/**
+ * 删除触发器
+ * @param index 触发器索引
+ */
+const removeTrigger = (index: number) => {
+  if (triggers.value.length > 1) {
+    triggers.value.splice(index, 1)
+  }
+}
+
+/**
+ * 更新触发器类型
+ * @param index 触发器索引
+ * @param type 触发器类型
+ */
+const updateTriggerType = (index: number, type: number) => {
+  triggers.value[index].type = type
+  onTriggerTypeChange(index, type)
+}
+
+/**
+ * 更新触发器设备配置
+ * @param index 触发器索引
+ * @param newTrigger 新的触发器对象
+ */
+const updateTriggerDeviceConfig = (index: number, newTrigger: Trigger) => {
+  triggers.value[index] = newTrigger
+}
+
+/**
+ * 更新触发器 CRON 配置
+ * @param index 触发器索引
+ * @param cronExpression CRON 表达式
+ */
+const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
+  triggers.value[index].cronExpression = cronExpression
+}
+
+/**
+ * 处理触发器类型变化事件
+ * @param index 触发器索引
+ * @param _ 触发器类型(未使用)
+ */
+const onTriggerTypeChange = (index: number, _: number) => {
+  const triggerItem = triggers.value[index]
+  triggerItem.productId = undefined
+  triggerItem.deviceId = undefined
+  triggerItem.identifier = undefined
+  triggerItem.operator = undefined
+  triggerItem.value = undefined
+  triggerItem.cronExpression = undefined
+  triggerItem.conditionGroups = []
+}
+
+/** 初始化:确保至少有一个触发器 */
+onMounted(() => {
+  if (triggers.value.length === 0) {
+    addTrigger()
+  }
+})
+</script>

+ 103 - 0
src/views/iot/rule/scene/form/selectors/DeviceSelector.vue

@@ -0,0 +1,103 @@
+<!-- 设备选择器组件 -->
+<template>
+  <el-select
+    :model-value="modelValue"
+    @update:model-value="handleChange"
+    placeholder="请选择设备"
+    filterable
+    clearable
+    class="w-full"
+    :loading="deviceLoading"
+    :disabled="!productId"
+  >
+    <el-option
+      v-for="device in deviceList"
+      :key="device.id"
+      :label="device.deviceName"
+      :value="device.id"
+    >
+      <div class="flex items-center justify-between w-full py-4px">
+        <div class="flex-1">
+          <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
+            {{ device.deviceName }}
+          </div>
+          <div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
+        </div>
+        <div class="flex items-center gap-4px" v-if="device.id > 0">
+          <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
+        </div>
+      </div>
+    </el-option>
+  </el-select>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { DEVICE_SELECTOR_OPTIONS } from '@/views/iot/utils/constants'
+import { DICT_TYPE } from '@/utils/dict'
+
+/** 设备选择器组件 */
+defineOptions({ name: 'DeviceSelector' })
+
+const props = defineProps<{
+  modelValue?: number
+  productId?: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value?: number): void
+  (e: 'change', value?: number): void
+}>()
+
+const deviceLoading = ref(false) // 设备加载状态
+const deviceList = ref<any[]>([]) // 设备列表
+
+/**
+ * 处理选择变化事件
+ * @param value 选中的设备ID
+ */
+const handleChange = (value?: number) => {
+  emit('update:modelValue', value)
+  emit('change', value)
+}
+
+/**
+ * 获取设备列表
+ */
+const getDeviceList = async () => {
+  if (!props.productId) {
+    deviceList.value = []
+    return
+  }
+
+  try {
+    deviceLoading.value = true
+    const res = await DeviceApi.getDeviceListByProductId(props.productId)
+    deviceList.value = res || []
+  } catch (error) {
+    console.error('获取设备列表失败:', error)
+    deviceList.value = []
+  } finally {
+    deviceList.value.unshift(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES)
+    deviceLoading.value = false
+  }
+}
+
+// 监听产品变化
+watch(
+  () => props.productId,
+  (newProductId) => {
+    if (newProductId) {
+      getDeviceList()
+    } else {
+      deviceList.value = []
+      // 清空当前选择的设备
+      if (props.modelValue) {
+        emit('update:modelValue', undefined)
+        emit('change', undefined)
+      }
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 264 - 0
src/views/iot/rule/scene/form/selectors/OperatorSelector.vue

@@ -0,0 +1,264 @@
+<!-- 操作符选择器组件 -->
+<template>
+  <div class="w-full">
+    <el-select
+      v-model="localValue"
+      placeholder="请选择操作符"
+      @change="handleChange"
+      class="w-full"
+    >
+      <el-option
+        v-for="operator in availableOperators"
+        :key="operator.value"
+        :label="operator.label"
+        :value="operator.value"
+      >
+        <div class="flex items-center justify-between w-full py-4px">
+          <div class="flex items-center gap-8px">
+            <div class="text-14px font-500 text-[var(--el-text-color-primary)]">
+              {{ operator.label }}
+            </div>
+            <div
+              class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono"
+            >
+              {{ operator.symbol }}
+            </div>
+          </div>
+          <div class="text-12px text-[var(--el-text-color-secondary)]">
+            {{ operator.description }}
+          </div>
+        </div>
+      </el-option>
+    </el-select>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import {
+  IotRuleSceneTriggerConditionParameterOperatorEnum,
+  IoTDataSpecsDataTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** 操作符选择器组件 */
+defineOptions({ name: 'OperatorSelector' })
+
+const props = defineProps<{
+  modelValue?: string
+  propertyType?: string
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void
+  (e: 'change', value: string): void
+}>()
+
+const localValue = useVModel(props, 'modelValue', emit)
+
+// 基于枚举的操作符定义
+const allOperators = [
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name,
+    symbol: '=',
+    description: '值完全相等时触发',
+    example: 'temperature = 25',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.TEXT,
+      IoTDataSpecsDataTypeEnum.BOOL,
+      IoTDataSpecsDataTypeEnum.ENUM
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name,
+    symbol: '≠',
+    description: '值不相等时触发',
+    example: 'power != false',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.TEXT,
+      IoTDataSpecsDataTypeEnum.BOOL,
+      IoTDataSpecsDataTypeEnum.ENUM
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.name,
+    symbol: '>',
+    description: '值大于指定值时触发',
+    example: 'temperature > 30',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.name,
+    symbol: '≥',
+    description: '值大于或等于指定值时触发',
+    example: 'humidity >= 80',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.name,
+    symbol: '<',
+    description: '值小于指定值时触发',
+    example: 'temperature < 10',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.name,
+    symbol: '≤',
+    description: '值小于或等于指定值时触发',
+    example: 'battery <= 20',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.name,
+    symbol: '∈',
+    description: '值在指定列表中时触发',
+    example: 'status in [1,2,3]',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.TEXT,
+      IoTDataSpecsDataTypeEnum.ENUM
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.name,
+    symbol: '∉',
+    description: '值不在指定列表中时触发',
+    example: 'status not in [1,2,3]',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.TEXT,
+      IoTDataSpecsDataTypeEnum.ENUM
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.name,
+    symbol: '⊆',
+    description: '值在指定范围内时触发',
+    example: 'temperature between 20,30',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.name,
+    symbol: '⊄',
+    description: '值不在指定范围内时触发',
+    example: 'temperature not between 20,30',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.name,
+    symbol: '≈',
+    description: '字符串匹配指定模式时触发',
+    example: 'message like "%error%"',
+    supportedTypes: [IoTDataSpecsDataTypeEnum.TEXT]
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.name,
+    symbol: '≠∅',
+    description: '值非空时触发',
+    example: 'data not null',
+    supportedTypes: [
+      IoTDataSpecsDataTypeEnum.INT,
+      IoTDataSpecsDataTypeEnum.FLOAT,
+      IoTDataSpecsDataTypeEnum.DOUBLE,
+      IoTDataSpecsDataTypeEnum.TEXT,
+      IoTDataSpecsDataTypeEnum.BOOL,
+      IoTDataSpecsDataTypeEnum.ENUM,
+      IoTDataSpecsDataTypeEnum.DATE
+    ]
+  }
+]
+
+// 计算属性:可用的操作符
+const availableOperators = computed(() => {
+  if (!props.propertyType) {
+    return allOperators
+  }
+  return allOperators.filter((op) =>
+    (op.supportedTypes as any[]).includes(props.propertyType || '')
+  )
+})
+
+// 计算属性:当前选中的操作符
+const selectedOperator = computed(() => {
+  return allOperators.find((op) => op.value === localValue.value)
+})
+
+/**
+ * 处理选择变化事件
+ * @param value 选中的操作符值
+ */
+const handleChange = (value: string) => {
+  emit('change', value)
+}
+
+/** 监听属性类型变化 */
+watch(
+  () => props.propertyType,
+  () => {
+    // 如果当前选择的操作符不支持新的属性类型,则清空选择
+    if (
+      localValue.value &&
+      selectedOperator.value &&
+      !(selectedOperator.value.supportedTypes as any[]).includes(props.propertyType || '')
+    ) {
+      localValue.value = ''
+    }
+  }
+)
+</script>
+
+<style scoped>
+:deep(.el-select-dropdown__item) {
+  height: auto;
+  padding: 8px 20px;
+}
+</style>

+ 79 - 0
src/views/iot/rule/scene/form/selectors/ProductSelector.vue

@@ -0,0 +1,79 @@
+<!-- 产品选择器组件 -->
+<template>
+  <el-select
+    :model-value="modelValue"
+    @update:model-value="handleChange"
+    placeholder="请选择产品"
+    filterable
+    clearable
+    class="w-full"
+    :loading="productLoading"
+  >
+    <el-option
+      v-for="product in productList"
+      :key="product.id"
+      :label="product.name"
+      :value="product.id"
+    >
+      <div class="flex items-center justify-between w-full py-4px">
+        <div class="flex-1">
+          <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
+            {{ product.name }}
+          </div>
+          <div class="text-12px text-[var(--el-text-color-secondary)]">
+            {{ product.productKey }}
+          </div>
+        </div>
+        <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
+      </div>
+    </el-option>
+  </el-select>
+</template>
+
+<script setup lang="ts">
+import { ProductApi } from '@/api/iot/product/product'
+import { DICT_TYPE } from '@/utils/dict'
+
+/** 产品选择器组件 */
+defineOptions({ name: 'ProductSelector' })
+
+defineProps<{
+  modelValue?: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value?: number): void
+  (e: 'change', value?: number): void
+}>()
+
+const productLoading = ref(false) // 产品加载状态
+const productList = ref<any[]>([]) // 产品列表
+
+/**
+ * 处理选择变化事件
+ * @param value 选中的产品 ID
+ */
+const handleChange = (value?: number) => {
+  emit('update:modelValue', value)
+  emit('change', value)
+}
+
+/** 获取产品列表 */
+const getProductList = async () => {
+  try {
+    productLoading.value = true
+    const res = await ProductApi.getSimpleProductList()
+    productList.value = res || []
+  } catch (error) {
+    console.error('获取产品列表失败:', error)
+    productList.value = []
+  } finally {
+    productLoading.value = false
+  }
+}
+
+// 组件挂载时获取产品列表
+onMounted(() => {
+  getProductList()
+})
+</script>

+ 437 - 0
src/views/iot/rule/scene/form/selectors/PropertySelector.vue

@@ -0,0 +1,437 @@
+<!-- 属性选择器组件 -->
+<template>
+  <div class="flex items-center gap-8px">
+    <el-select
+      v-model="localValue"
+      placeholder="请选择监控项"
+      filterable
+      clearable
+      @change="handleChange"
+      class="!w-150px"
+      :loading="loading"
+    >
+      <el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
+        <el-option
+          v-for="property in group.options"
+          :key="property.identifier"
+          :label="property.name"
+          :value="property.identifier"
+        >
+          <div class="flex items-center justify-between w-full py-2px">
+            <span class="text-14px font-500 text-[var(--el-text-color-primary)] flex-1 truncate">
+              {{ property.name }}
+            </span>
+            <el-tag
+              :type="getDataTypeTagType(property.dataType)"
+              size="small"
+              class="ml-8px flex-shrink-0"
+            >
+              {{ property.identifier }}
+            </el-tag>
+          </div>
+        </el-option>
+      </el-option-group>
+    </el-select>
+
+    <!-- 属性详情弹出层 -->
+    <el-popover
+      v-if="selectedProperty"
+      placement="right-start"
+      :width="350"
+      trigger="click"
+      :show-arrow="true"
+      :offset="8"
+      popper-class="property-detail-popover"
+    >
+      <template #reference>
+        <el-button
+          type="info"
+          :icon="InfoFilled"
+          circle
+          size="small"
+          class="flex-shrink-0"
+          title="查看属性详情"
+        />
+      </template>
+
+      <!-- 弹出层内容 -->
+      <div class="property-detail-content">
+        <div class="flex items-center gap-8px mb-12px">
+          <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
+          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+            {{ selectedProperty.name }}
+          </span>
+          <el-tag :type="getDataTypeTagType(selectedProperty.dataType)" size="small">
+            {{ getDataTypeName(selectedProperty.dataType) }}
+          </el-tag>
+        </div>
+
+        <div class="space-y-8px ml-24px">
+          <div class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              标识符:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.identifier }}
+            </span>
+          </div>
+
+          <div v-if="selectedProperty.description" class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              描述:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.description }}
+            </span>
+          </div>
+
+          <div v-if="selectedProperty.unit" class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              单位:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.unit }}
+            </span>
+          </div>
+
+          <div v-if="selectedProperty.range" class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              取值范围:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.range }}
+            </span>
+          </div>
+
+          <!-- 根据属性类型显示额外信息 -->
+          <div
+            v-if="
+              selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
+              selectedProperty.accessMode
+            "
+            class="flex items-start gap-8px"
+          >
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              访问模式:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ getAccessModeLabel(selectedProperty.accessMode) }}
+            </span>
+          </div>
+
+          <div
+            v-if="
+              selectedProperty.type === IoTThingModelTypeEnum.EVENT && selectedProperty.eventType
+            "
+            class="flex items-start gap-8px"
+          >
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              事件类型:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ getEventTypeLabel(selectedProperty.eventType) }}
+            </span>
+          </div>
+
+          <div
+            v-if="
+              selectedProperty.type === IoTThingModelTypeEnum.SERVICE && selectedProperty.callType
+            "
+            class="flex items-start gap-8px"
+          >
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              调用类型:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
+            </span>
+          </div>
+        </div>
+      </div>
+    </el-popover>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { InfoFilled } from '@element-plus/icons-vue'
+import {
+  IotRuleSceneTriggerTypeEnum,
+  IoTThingModelTypeEnum,
+  getAccessModeLabel,
+  getEventTypeLabel,
+  getThingModelServiceCallTypeLabel,
+  getDataTypeName,
+  getDataTypeTagType,
+  THING_MODEL_GROUP_LABELS
+} from '@/views/iot/utils/constants'
+import type {
+  IotThingModelTSLResp,
+  ThingModelEvent,
+  ThingModelParam,
+  ThingModelProperty,
+  ThingModelService
+} from '@/api/iot/thingmodel'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+
+/** 属性选择器组件 */
+defineOptions({ name: 'PropertySelector' })
+
+/** 属性选择器内部使用的统一数据结构 */
+interface PropertySelectorItem {
+  identifier: string
+  name: string
+  description?: string
+  dataType: string
+  type: number // IoTThingModelTypeEnum
+  accessMode?: string
+  required?: boolean
+  unit?: string
+  range?: string
+  eventType?: string
+  callType?: string
+  inputParams?: ThingModelParam[]
+  outputParams?: ThingModelParam[]
+  property?: ThingModelProperty
+  event?: ThingModelEvent
+  service?: ThingModelService
+}
+
+const props = defineProps<{
+  modelValue?: string
+  triggerType: number
+  productId?: number
+  deviceId?: number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void
+  (e: 'change', value: { type: string; config: any }): void
+}>()
+
+const localValue = useVModel(props, 'modelValue', emit)
+
+const loading = ref(false) // 加载状态
+const propertyList = ref<PropertySelectorItem[]>([]) // 属性列表
+const thingModelTSL = ref<IotThingModelTSLResp | null>(null) // 物模型TSL数据
+
+// 计算属性:属性分组
+const propertyGroups = computed(() => {
+  const groups: { label: string; options: any[] }[] = []
+
+  if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
+    groups.push({
+      label: THING_MODEL_GROUP_LABELS.PROPERTY,
+      options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.PROPERTY)
+    })
+  }
+
+  if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
+    groups.push({
+      label: THING_MODEL_GROUP_LABELS.EVENT,
+      options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.EVENT)
+    })
+  }
+
+  if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
+    groups.push({
+      label: THING_MODEL_GROUP_LABELS.SERVICE,
+      options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.SERVICE)
+    })
+  }
+
+  return groups.filter((group) => group.options.length > 0)
+})
+
+// 计算属性:当前选中的属性
+const selectedProperty = computed(() => {
+  return propertyList.value.find((p) => p.identifier === localValue.value)
+})
+
+/**
+ * 处理选择变化事件
+ * @param value 选中的属性标识符
+ */
+const handleChange = (value: string) => {
+  const property = propertyList.value.find((p) => p.identifier === value)
+  if (property) {
+    emit('change', {
+      type: property.dataType,
+      config: property
+    })
+  }
+}
+
+/**
+ * 获取物模型TSL数据
+ */
+const getThingModelTSL = async () => {
+  if (!props.productId) {
+    thingModelTSL.value = null
+    propertyList.value = []
+    return
+  }
+
+  loading.value = true
+  try {
+    const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
+
+    if (tslData) {
+      thingModelTSL.value = tslData
+      parseThingModelData()
+    } else {
+      console.error('获取物模型TSL失败: 返回数据为空')
+      propertyList.value = []
+    }
+  } catch (error) {
+    console.error('获取物模型TSL失败:', error)
+    propertyList.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 解析物模型 TSL 数据 */
+const parseThingModelData = () => {
+  const tsl = thingModelTSL.value
+  const properties: PropertySelectorItem[] = []
+
+  if (!tsl) {
+    propertyList.value = properties
+    return
+  }
+  // 解析属性
+  if (tsl.properties && Array.isArray(tsl.properties)) {
+    tsl.properties.forEach((prop) => {
+      properties.push({
+        identifier: prop.identifier,
+        name: prop.name,
+        description: prop.description,
+        dataType: prop.dataType,
+        type: IoTThingModelTypeEnum.PROPERTY,
+        accessMode: prop.accessMode,
+        required: prop.required,
+        unit: getPropertyUnit(prop),
+        range: getPropertyRange(prop),
+        property: prop
+      })
+    })
+  }
+
+  // 解析事件
+  if (tsl.events && Array.isArray(tsl.events)) {
+    tsl.events.forEach((event) => {
+      properties.push({
+        identifier: event.identifier,
+        name: event.name,
+        description: event.description,
+        dataType: 'struct',
+        type: IoTThingModelTypeEnum.EVENT,
+        eventType: event.type,
+        required: event.required,
+        outputParams: event.outputParams,
+        event: event
+      })
+    })
+  }
+
+  // 解析服务
+  if (tsl.services && Array.isArray(tsl.services)) {
+    tsl.services.forEach((service) => {
+      properties.push({
+        identifier: service.identifier,
+        name: service.name,
+        description: service.description,
+        dataType: 'struct',
+        type: IoTThingModelTypeEnum.SERVICE,
+        callType: service.callType,
+        required: service.required,
+        inputParams: service.inputParams,
+        outputParams: service.outputParams,
+        service: service
+      })
+    })
+  }
+  propertyList.value = properties
+}
+
+/**
+ * 获取属性单位
+ * @param property 属性对象
+ * @returns 属性单位
+ */
+const getPropertyUnit = (property: any) => {
+  if (!property) return undefined
+
+  // 数值型数据的单位
+  if (property.dataSpecs && property.dataSpecs.unit) {
+    return property.dataSpecs.unit
+  }
+
+  return undefined
+}
+
+/**
+ * 获取属性范围描述
+ * @param property 属性对象
+ * @returns 属性范围描述
+ */
+const getPropertyRange = (property: any) => {
+  if (!property) return undefined
+
+  // 数值型数据的范围
+  if (property.dataSpecs) {
+    const specs = property.dataSpecs
+    if (specs.min !== undefined && specs.max !== undefined) {
+      return `${specs.min}~${specs.max}`
+    }
+  }
+
+  // 枚举型和布尔型数据的选项
+  if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
+    return property.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
+  }
+
+  return undefined
+}
+
+/** 监听产品变化 */
+watch(
+  () => props.productId,
+  () => {
+    getThingModelTSL()
+  },
+  { immediate: true }
+)
+
+/** 监听触发类型变化 */
+watch(
+  () => props.triggerType,
+  () => {
+    localValue.value = ''
+  }
+)
+</script>
+
+<style scoped>
+/* 下拉选项样式 */
+:deep(.el-select-dropdown__item) {
+  height: auto;
+  padding: 6px 20px;
+}
+
+/* 弹出层内容样式 */
+.property-detail-content {
+  padding: 4px 0;
+}
+
+/* 弹出层自定义样式 */
+:global(.property-detail-popover) {
+  /* 可以在这里添加全局弹出层样式 */
+  max-width: 400px !important;
+}
+
+:global(.property-detail-popover .el-popover__content) {
+  padding: 16px !important;
+}
+</style>

+ 494 - 0
src/views/iot/rule/scene/index.vue

@@ -0,0 +1,494 @@
+<template>
+  <ContentWrap>
+    <!-- 页面头部 -->
+    <div class="flex justify-between items-start mb-20px">
+      <div class="flex-1">
+        <h2 class="flex items-center m-0 mb-8px text-24px font-600 text-[#303133]">
+          <Icon icon="ep:connection" class="ml-5px mr-12px text-[#409eff]" />
+          场景联动规则
+        </h2>
+        <p class="m-0 text-[#606266] text-14px">
+          通过配置触发条件和执行动作,实现设备间的智能联动控制
+        </p>
+      </div>
+      <div>
+        <el-button type="primary" @click="handleAdd">
+          <Icon icon="ep:plus" />
+          新增规则
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 搜索和筛选 -->
+    <el-card class="mb-16px" shadow="never">
+      <el-form
+        ref="queryFormRef"
+        :model="queryParams"
+        :inline="true"
+        label-width="80px"
+        @submit.prevent
+      >
+        <el-form-item label="规则名称">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="请输入规则名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item label="规则状态">
+          <el-select
+            v-model="queryParams.status"
+            placeholder="请选择状态"
+            clearable
+            class="!w-240px"
+          >
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" />
+            重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="mb-16px">
+      <el-col :span="6">
+        <el-card
+          class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+          shadow="hover"
+        >
+          <div class="flex items-center">
+            <div
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#667eea] to-[#764ba2]"
+            >
+              <Icon icon="ep:document" />
+            </div>
+            <div>
+              <div class="text-24px font-600 text-[#303133] leading-none">{{
+                statistics.total
+              }}</div>
+              <div class="text-14px text-[#909399] mt-4px">总规则数</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card
+          class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+          shadow="hover"
+        >
+          <div class="flex items-center">
+            <div
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#f093fb] to-[#f5576c]"
+            >
+              <Icon icon="ep:check" />
+            </div>
+            <div>
+              <div class="text-24px font-600 text-[#303133] leading-none">{{
+                statistics.enabled
+              }}</div>
+              <div class="text-14px text-[#909399] mt-4px">启用规则</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card
+          class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+          shadow="hover"
+        >
+          <div class="flex items-center">
+            <div
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#4facfe] to-[#00f2fe]"
+            >
+              <Icon icon="ep:close" />
+            </div>
+            <div>
+              <div class="text-24px font-600 text-[#303133] leading-none">{{
+                statistics.disabled
+              }}</div>
+              <div class="text-14px text-[#909399] mt-4px">禁用规则</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card
+          class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+          shadow="hover"
+        >
+          <div class="flex items-center">
+            <div
+              class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#43e97b] to-[#38f9d7]"
+            >
+              <Icon icon="ep:timer" />
+            </div>
+            <div>
+              <div class="text-24px font-600 text-[#303133] leading-none">{{
+                statistics.timerRules
+              }}</div>
+              <div class="text-14px text-[#909399] mt-4px">定时规则</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-card class="mb-20px" shadow="never">
+      <el-table v-loading="loading" :data="list" stripe @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" />
+        <el-table-column label="规则名称" prop="name" min-width="200">
+          <template #default="{ row }">
+            <div class="flex items-center gap-8px">
+              <span class="font-500 text-[#303133]">{{ row.name }}</span>
+              <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
+            </div>
+            <div v-if="row.description" class="text-12px text-[#909399] mt-4px">
+              {{ row.description }}
+            </div>
+          </template>
+        </el-table-column>
+        <!-- 触发条件列 -->
+        <el-table-column label="触发条件" min-width="280">
+          <template #default="{ row }">
+            <div class="space-y-4px">
+              <div class="flex flex-wrap gap-4px">
+                <el-tag type="primary" size="small" class="m-0">
+                  {{ getTriggerSummary(row) }}
+                </el-tag>
+              </div>
+              <!-- 显示定时触发器的额外信息 -->
+              <div v-if="hasTimerTrigger(row)" class="mt-4px">
+                <el-tooltip :content="getCronExpression(row)" placement="top">
+                  <el-tag size="small" type="info" class="mr-4px">
+                    <Icon icon="ep:timer" class="mr-2px" />
+                    {{ getCronFrequency(row) }}
+                  </el-tag>
+                </el-tooltip>
+                <div v-if="getNextExecutionTime(row)" class="text-12px text-[#909399] mt-2px">
+                  <Icon icon="ep:clock" class="mr-2px" />
+                  下次执行: {{ formatDate(getNextExecutionTime(row)!) }}
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <!-- 执行动作列 -->
+        <el-table-column label="执行动作" min-width="250">
+          <template #default="{ row }">
+            <div class="flex flex-wrap gap-4px">
+              <el-tag type="success" size="small" class="m-0">
+                {{ getActionSummary(row) }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="最近触发" prop="lastTriggeredTime" width="180">
+          <template #default="{ row }">
+            <span v-if="row.lastTriggeredTime">
+              {{ formatDate(row.lastTriggeredTime) }}
+            </span>
+            <span v-else class="text-gray-400">未触发</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" prop="createTime" width="180">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="210" fixed="right">
+          <template #default="{ row }">
+            <div class="flex gap-8px">
+              <el-button type="primary" link @click="handleEdit(row)">
+                <Icon icon="ep:edit" />
+                编辑
+              </el-button>
+              <el-button
+                :type="row.status === 0 ? 'warning' : 'success'"
+                link
+                @click="handleToggleStatus(row)"
+              >
+                <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
+                {{ getDictLabel(DICT_TYPE.COMMON_STATUS, row.status) }}
+              </el-button>
+              <el-button type="danger" class="!mr-10px" link @click="handleDelete(row.id)">
+                <Icon icon="ep:delete" />
+                删除
+              </el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 表单对话框 -->
+    <RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getDictLabel, getIntDictOptions } from '@/utils/dict'
+import { ContentWrap } from '@/components/ContentWrap'
+import RuleSceneForm from './form/RuleSceneForm.vue'
+import { IotSceneRule, RuleSceneApi } from '@/api/iot/rule/scene'
+import {
+  getActionTypeLabel,
+  getTriggerTypeLabel,
+  IotRuleSceneTriggerTypeEnum
+} from '@/views/iot/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import { CommonStatusEnum } from '@/utils/constants'
+import { CronUtils } from '@/utils/cron'
+
+/** 场景联动规则管理页面 */
+defineOptions({ name: 'IoTSceneRule' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+/** 查询参数 */
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  status: undefined
+})
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IotSceneRule[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const selectedRows = ref<IotSceneRule[]>([]) // 选中的行数据
+const queryFormRef = ref() // 搜索的表单
+
+/** 表单状态 */
+const formVisible = ref(false) // 是否可见
+const currentRule = ref<IotSceneRule>() // 表单数据
+
+/** 统计数据 */
+const statistics = ref({
+  total: 0,
+  enabled: 0,
+  disabled: 0,
+  triggered: 0, // 已触发的规则数量 (暂时使用启用状态的规则数量)
+  timerRules: 0 // 定时规则数量
+})
+
+/** 获取规则摘要信息 */
+const getRuleSceneSummary = (rule: IotSceneRule) => {
+  const triggerSummary =
+    rule.triggers?.map((trigger: any) => {
+      // 构建基础描述
+      let description = getTriggerTypeLabel(trigger.type)
+      switch (trigger.type) {
+        case IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE:
+          break
+        case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
+        case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
+        case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
+          if (trigger.identifier) {
+            description += ` (${trigger.identifier})`
+          }
+          break
+        case IotRuleSceneTriggerTypeEnum.TIMER:
+          description = `${getTriggerTypeLabel(trigger.type)} (${CronUtils.format(trigger.cronExpression || '')})`
+          break
+        default:
+          description = getTriggerTypeLabel(trigger.type)
+      }
+      // 添加设备信息(如果有)
+      if (trigger.deviceId) {
+        description += ` [设备ID: ${trigger.deviceId}]`
+      } else if (trigger.productId) {
+        description += ` [产品ID: ${trigger.productId}]`
+      }
+      return description
+    }) || []
+
+  const actionSummary =
+    rule.actions?.map((action: any) => {
+      // 构建基础描述
+      let description = getActionTypeLabel(action.type)
+      // 添加设备信息(如果有)
+      if (action.deviceId) {
+        description += ` [设备ID: ${action.deviceId}]`
+      } else if (action.productId) {
+        description += ` [产品ID: ${action.productId}]`
+      }
+      // 添加告警配置信息(如果有)
+      if (action.alertConfigId) {
+        description += ` [告警配置ID: ${action.alertConfigId}]`
+      }
+      return description
+    }) || []
+
+  return {
+    triggerSummary: triggerSummary.join(', ') || '无触发器',
+    actionSummary: actionSummary.join(', ') || '无执行器'
+  }
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await RuleSceneApi.getRuleScenePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    // 更新统计数据
+    updateStatistics()
+    loading.value = false
+  }
+}
+
+/** 更新统计数据 */
+const updateStatistics = () => {
+  statistics.value = {
+    total: list.value.length,
+    enabled: list.value.filter((item) => item.status === CommonStatusEnum.ENABLE).length,
+    disabled: list.value.filter((item) => item.status === CommonStatusEnum.DISABLE).length,
+    triggered: list.value.filter((item) => item.status === CommonStatusEnum.ENABLE).length,
+    timerRules: list.value.filter((item) => hasTimerTrigger(item)).length
+  }
+}
+
+/** 获取触发器摘要 */
+const getTriggerSummary = (rule: IotSceneRule) => {
+  return getRuleSceneSummary(rule).triggerSummary
+}
+
+/** 获取执行器摘要 */
+const getActionSummary = (rule: IotSceneRule) => {
+  return getRuleSceneSummary(rule).actionSummary
+}
+
+/** 检查规则是否包含定时触发器 */
+const hasTimerTrigger = (rule: IotSceneRule): boolean => {
+  return (
+    rule.triggers?.some((trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) || false
+  )
+}
+
+/** 获取 CRON 表达式的执行频率描述 */
+const getCronFrequency = (rule: IotSceneRule): string => {
+  const timerTrigger = rule.triggers?.find(
+    (trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+  )
+  if (timerTrigger?.cronExpression) {
+    return CronUtils.getFrequencyDescription(timerTrigger.cronExpression)
+  }
+  return ''
+}
+
+/** 获取下次执行时间 */
+const getNextExecutionTime = (rule: IotSceneRule): Date | null => {
+  const timerTrigger = rule.triggers?.find(
+    (trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+  )
+  if (timerTrigger?.cronExpression) {
+    return CronUtils.getNextExecutionTime(timerTrigger.cronExpression)
+  }
+  return null
+}
+
+/** 获取 CRON 表达式原始值 */
+const getCronExpression = (rule: IotSceneRule): string => {
+  const timerTrigger = rule.triggers?.find(
+    (trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+  )
+  return timerTrigger?.cronExpression || ''
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.name = ''
+  queryParams.status = undefined
+  handleQuery()
+}
+
+/** 添加操作 */
+const handleAdd = () => {
+  currentRule.value = undefined
+  formVisible.value = true
+}
+
+/** 修改操作 */
+const handleEdit = (row: IotSceneRule) => {
+  currentRule.value = row
+  formVisible.value = true
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await RuleSceneApi.deleteRuleScene(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch (error) {}
+}
+
+/** 修改状态 */
+const handleToggleStatus = async (row: IotSceneRule) => {
+  try {
+    // 修改状态的二次确认
+    const text = row.status === CommonStatusEnum.ENABLE ? '禁用' : '启用'
+    await message.confirm('确认要' + text + '"' + row.name + '"吗?')
+    // 发起修改状态
+    await RuleSceneApi.updateRuleSceneStatus(
+      row.id!,
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+    )
+    message.success(text + '成功')
+    // 刷新
+    await getList()
+  } catch {
+    // 取消后,进行恢复按钮
+    row.status =
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+  }
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: IotSceneRule[]) => {
+  selectedRows.value = selection
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 13 - 11
src/views/iot/thingmodel/ThingModelEvent.vue

@@ -6,21 +6,19 @@
     prop="event.type"
   >
     <el-radio-group v-model="thingModelEvent.type">
-      <el-radio :value="ThingModelEventType.INFO.value">
-        {{ ThingModelEventType.INFO.label }}
-      </el-radio>
-      <el-radio :value="ThingModelEventType.ALERT.value">
-        {{ ThingModelEventType.ALERT.label }}
-      </el-radio>
-      <el-radio :value="ThingModelEventType.ERROR.value">
-        {{ ThingModelEventType.ERROR.label }}
+      <el-radio
+        v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
+        :key="eventType.value"
+        :value="eventType.value"
+      >
+        {{ eventType.label }}
       </el-radio>
     </el-radio-group>
   </el-form-item>
   <el-form-item label="输出参数">
     <ThingModelInputOutputParam
       v-model="thingModelEvent.outputParams"
-      :direction="ThingModelParamDirection.OUTPUT"
+      :direction="IoTThingModelParamDirectionEnum.OUTPUT"
     />
   </el-form-item>
 </template>
@@ -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<ThingModelE
 // 默认选中,INFO 信息
 watch(
   () => 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 }
 )
 </script>

+ 31 - 24
src/views/iot/thingmodel/ThingModelForm.vue

@@ -27,16 +27,19 @@
       </el-form-item>
       <!-- 属性配置 -->
       <ThingModelProperty
-        v-if="formData.type === ThingModelType.PROPERTY"
+        v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
         v-model="formData.property"
       />
       <!-- 服务配置 -->
       <ThingModelService
-        v-if="formData.type === ThingModelType.SERVICE"
+        v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
         v-model="formData.service"
       />
       <!-- 事件配置 -->
-      <ThingModelEvent v-if="formData.type === ThingModelType.EVENT" v-model="formData.event" />
+      <ThingModelEvent
+        v-if="formData.type === IoTThingModelTypeEnum.EVENT"
+        v-model="formData.event"
+      />
       <el-form-item label="描述" prop="description">
         <el-input
           v-model="formData.description"
@@ -60,9 +63,12 @@ import { ProductVO } from '@/api/iot/product/product'
 import ThingModelProperty from './ThingModelProperty.vue'
 import ThingModelService from './ThingModelService.vue'
 import ThingModelEvent from './ThingModelEvent.vue'
-import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
-import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
-import { DataSpecsDataType, ThingModelFormRules, ThingModelType } from './config'
+import { ThingModelApi, ThingModelData, ThingModelFormRules } from '@/api/iot/thingmodel'
+import {
+  IOT_PROVIDE_KEY,
+  IoTDataSpecsDataTypeEnum,
+  IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
 import { cloneDeep } from 'lodash-es'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { isEmpty } from '@/utils/is'
@@ -80,12 +86,12 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref<ThingModelData>({
-  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: {},

+ 0 - 0
src/views/iot/thingmodel/ThingModelInputOutputParam.vue


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff