Pre Merge pull request !738 from 芋道源码/feature/iot
						commit
						b32fce70f7
					
				|  | @ -87,7 +87,7 @@ | |||
|     "source.fixAll.stylelint": "explicit" | ||||
|   }, | ||||
|   "[vue]": { | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|     "editor.defaultFormatter": "octref.vetur" | ||||
|   }, | ||||
|   "i18n-ally.localesPaths": ["src/locales"], | ||||
|   "i18n-ally.keystyle": "nested", | ||||
|  |  | |||
|  | @ -67,6 +67,7 @@ | |||
|     "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", | ||||
|  |  | |||
|  | @ -0,0 +1,169 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // IoT 设备 VO
 | ||||
| export interface DeviceVO { | ||||
|   id: number // 设备 ID,主键,自增
 | ||||
|   deviceKey: string // 设备唯一标识符
 | ||||
|   deviceName: string // 设备名称
 | ||||
|   productId: number // 产品编号
 | ||||
|   productKey: string // 产品标识
 | ||||
|   deviceType: number // 设备类型
 | ||||
|   nickname: string // 设备备注名称
 | ||||
|   gatewayId: number // 网关设备 ID
 | ||||
|   state: number // 设备状态
 | ||||
|   onlineTime: Date // 最后上线时间
 | ||||
|   offlineTime: Date // 最后离线时间
 | ||||
|   activeTime: Date // 设备激活时间
 | ||||
|   createTime: Date // 创建时间
 | ||||
|   ip: string // 设备的 IP 地址
 | ||||
|   firmwareVersion: string // 设备的固件版本
 | ||||
|   deviceSecret: string // 设备密钥,用于设备认证,需安全存储
 | ||||
|   mqttClientId: string // MQTT 客户端 ID
 | ||||
|   mqttUsername: string // MQTT 用户名
 | ||||
|   mqttPassword: string // MQTT 密码
 | ||||
|   authType: string // 认证类型
 | ||||
|   latitude: number // 设备位置的纬度
 | ||||
|   longitude: number // 设备位置的经度
 | ||||
|   areaId: number // 地区编码
 | ||||
|   address: string // 设备详细地址
 | ||||
|   serialNumber: string // 设备序列号
 | ||||
|   config: string // 设备配置
 | ||||
|   groupIds?: number[] // 添加分组 ID
 | ||||
| } | ||||
| 
 | ||||
| // IoT 设备数据 VO
 | ||||
| export interface DeviceDataVO { | ||||
|   deviceId: number // 设备编号
 | ||||
|   thinkModelFunctionId: number // 物模型编号
 | ||||
|   productKey: string // 产品标识
 | ||||
|   deviceName: string // 设备名称
 | ||||
|   identifier: string // 属性标识符
 | ||||
|   name: string // 属性名称
 | ||||
|   dataType: string // 数据类型
 | ||||
|   updateTime: Date // 更新时间
 | ||||
|   value: string // 最新值
 | ||||
| } | ||||
| 
 | ||||
| // IoT 设备数据 VO
 | ||||
| export interface DeviceHistoryDataVO { | ||||
|   time: number // 时间
 | ||||
|   data: string // 数据
 | ||||
| } | ||||
| 
 | ||||
| // IoT 设备状态枚举
 | ||||
| export enum DeviceStateEnum { | ||||
|   INACTIVE = 0, // 未激活
 | ||||
|   ONLINE = 1, // 在线
 | ||||
|   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 // 请求参数
 | ||||
| } | ||||
| 
 | ||||
| // MQTT 连接参数 VO
 | ||||
| export interface MqttConnectionParamsVO { | ||||
|   mqttClientId: string // MQTT 客户端 ID
 | ||||
|   mqttUsername: string // MQTT 用户名
 | ||||
|   mqttPassword: string // MQTT 密码
 | ||||
| } | ||||
| 
 | ||||
| // 设备 API
 | ||||
| export const DeviceApi = { | ||||
|   // 查询设备分页
 | ||||
|   getDevicePage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/device/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询设备详情
 | ||||
|   getDevice: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/device/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增设备
 | ||||
|   createDevice: async (data: DeviceVO) => { | ||||
|     return await request.post({ url: `/iot/device/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改设备
 | ||||
|   updateDevice: async (data: DeviceVO) => { | ||||
|     return await request.put({ url: `/iot/device/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改设备分组
 | ||||
|   updateDeviceGroup: async (data: { ids: number[]; groupIds: number[] }) => { | ||||
|     return await request.put({ url: `/iot/device/update-group`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除单个设备
 | ||||
|   deleteDevice: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/device/delete?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除多个设备
 | ||||
|   deleteDeviceList: async (ids: number[]) => { | ||||
|     return await request.delete({ url: `/iot/device/delete-list`, params: { ids: ids.join(',') } }) | ||||
|   }, | ||||
| 
 | ||||
|   // 导出设备
 | ||||
|   exportDeviceExcel: async (params: any) => { | ||||
|     return await request.download({ url: `/iot/device/export-excel`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备数量
 | ||||
|   getDeviceCount: async (productId: number) => { | ||||
|     return await request.get({ url: `/iot/device/count?productId=` + productId }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备的精简信息列表
 | ||||
|   getSimpleDeviceList: async (deviceType?: number) => { | ||||
|     return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取导入模板
 | ||||
|   importDeviceTemplate: async () => { | ||||
|     return await request.download({ url: `/iot/device/get-import-template` }) | ||||
|   }, | ||||
| 
 | ||||
|   // 设备上行
 | ||||
|   upstreamDevice: async (data: IotDeviceUpstreamReqVO) => { | ||||
|     return await request.post({ url: `/iot/device/upstream`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 设备下行
 | ||||
|   downstreamDevice: async (data: IotDeviceDownstreamReqVO) => { | ||||
|     return await request.post({ url: `/iot/device/downstream`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备属性最新数据
 | ||||
|   getLatestDeviceProperties: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/device/property/latest`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备属性历史数据
 | ||||
|   getHistoryDevicePropertyPage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/device/property/history-page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询设备日志分页
 | ||||
|   getDeviceLogPage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/device/log/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备MQTT连接参数
 | ||||
|   getMqttConnectionParams: async (deviceId: number) => { | ||||
|     return await request.get({ url: `/iot/device/mqtt-connection-params`, params: { deviceId } }) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // IoT 设备分组 VO
 | ||||
| export interface DeviceGroupVO { | ||||
|   id: number // 分组 ID
 | ||||
|   name: string // 分组名字
 | ||||
|   status: number // 分组状态
 | ||||
|   description: string // 分组描述
 | ||||
|   deviceCount?: number // 设备数量
 | ||||
| } | ||||
| 
 | ||||
| // IoT 设备分组 API
 | ||||
| export const DeviceGroupApi = { | ||||
|   // 查询设备分组分页
 | ||||
|   getDeviceGroupPage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/device-group/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询设备分组详情
 | ||||
|   getDeviceGroup: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/device-group/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增设备分组
 | ||||
|   createDeviceGroup: async (data: DeviceGroupVO) => { | ||||
|     return await request.post({ url: `/iot/device-group/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改设备分组
 | ||||
|   updateDeviceGroup: async (data: DeviceGroupVO) => { | ||||
|     return await request.put({ url: `/iot/device-group/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除设备分组
 | ||||
|   deleteDeviceGroup: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/device-group/delete?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备分组的精简信息列表
 | ||||
|   getSimpleDeviceGroupList: async () => { | ||||
|     return await request.get({ url: `/iot/device-group/simple-list` }) | ||||
|   } | ||||
| } | ||||
|  | @ -1,74 +0,0 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // IoT 设备 VO
 | ||||
| export interface DeviceVO { | ||||
|   id: number // 设备 ID,主键,自增
 | ||||
|   deviceKey: string // 设备唯一标识符
 | ||||
|   deviceName: string // 设备名称
 | ||||
|   productId: number // 产品编号
 | ||||
|   productKey: string // 产品标识
 | ||||
|   deviceType: number // 设备类型
 | ||||
|   nickname: string // 设备备注名称
 | ||||
|   gatewayId: number // 网关设备 ID
 | ||||
|   status: number // 设备状态
 | ||||
|   statusLastUpdateTime: Date // 设备状态最后更新时间
 | ||||
|   lastOnlineTime: Date // 最后上线时间
 | ||||
|   lastOfflineTime: Date // 最后离线时间
 | ||||
|   activeTime: Date // 设备激活时间
 | ||||
|   createTime: Date // 创建时间
 | ||||
|   ip: string // 设备的 IP 地址
 | ||||
|   firmwareVersion: string // 设备的固件版本
 | ||||
|   deviceSecret: string // 设备密钥,用于设备认证,需安全存储
 | ||||
|   mqttClientId: string // MQTT 客户端 ID
 | ||||
|   mqttUsername: string // MQTT 用户名
 | ||||
|   mqttPassword: string // MQTT 密码
 | ||||
|   authType: string // 认证类型
 | ||||
|   latitude: number // 设备位置的纬度
 | ||||
|   longitude: number // 设备位置的经度
 | ||||
|   areaId: number // 地区编码
 | ||||
|   address: string // 设备详细地址
 | ||||
|   serialNumber: string // 设备序列号
 | ||||
| } | ||||
| 
 | ||||
| export interface DeviceUpdateStatusVO { | ||||
|   id: number // 设备 ID,主键,自增
 | ||||
|   status: number // 设备状态
 | ||||
| } | ||||
| 
 | ||||
| // 设备 API
 | ||||
| export const DeviceApi = { | ||||
|   // 查询设备分页
 | ||||
|   getDevicePage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/device/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询设备详情
 | ||||
|   getDevice: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/device/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增设备
 | ||||
|   createDevice: async (data: DeviceVO) => { | ||||
|     return await request.post({ url: `/iot/device/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改设备
 | ||||
|   updateDevice: async (data: DeviceVO) => { | ||||
|     return await request.put({ url: `/iot/device/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改设备状态
 | ||||
|   updateDeviceStatus: async (data: DeviceUpdateStatusVO) => { | ||||
|     return await request.put({ url: `/iot/device/update-status`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除设备
 | ||||
|   deleteDevice: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/device/delete?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获取设备数量
 | ||||
|   getDeviceCount: async (productId: number) => { | ||||
|     return await request.get({ url: `/iot/device/count?productId=` + productId }) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,51 @@ | |||
| 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 }) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // IoT 产品分类 VO
 | ||||
| export interface ProductCategoryVO { | ||||
|   id: number // 分类 ID
 | ||||
|   name: string // 分类名字
 | ||||
|   sort: number // 分类排序
 | ||||
|   status: number // 分类状态
 | ||||
|   description: string // 分类描述
 | ||||
| } | ||||
| 
 | ||||
| // IoT 产品分类 API
 | ||||
| export const ProductCategoryApi = { | ||||
|   // 查询产品分类分页
 | ||||
|   getProductCategoryPage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/product-category/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询产品分类详情
 | ||||
|   getProductCategory: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/product-category/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增产品分类
 | ||||
|   createProductCategory: async (data: ProductCategoryVO) => { | ||||
|     return await request.post({ url: `/iot/product-category/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改产品分类
 | ||||
|   updateProductCategory: async (data: ProductCategoryVO) => { | ||||
|     return await request.put({ url: `/iot/product-category/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除产品分类
 | ||||
|   deleteProductCategory: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/product-category/delete?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   /** 获取产品分类精简列表 */ | ||||
|   getSimpleProductCategoryList: () => { | ||||
|     return request.get({ url: '/iot/product-category/simple-list' }) | ||||
|   } | ||||
| } | ||||
|  | @ -7,6 +7,9 @@ export interface ProductVO { | |||
|   productKey: string // 产品标识
 | ||||
|   protocolId: number // 协议编号
 | ||||
|   categoryId: number // 产品所属品类标识符
 | ||||
|   categoryName?: string // 产品所属品类名称
 | ||||
|   icon: string // 产品图标
 | ||||
|   picUrl: string // 产品图片
 | ||||
|   description: string // 产品描述
 | ||||
|   validateType: number // 数据校验级别
 | ||||
|   status: number // 产品状态
 | ||||
|  | @ -18,6 +21,23 @@ export interface ProductVO { | |||
|   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 产品 API
 | ||||
| export const ProductApi = { | ||||
|   // 查询产品分页
 | ||||
|  | @ -57,6 +77,6 @@ export const ProductApi = { | |||
| 
 | ||||
|   // 查询产品(精简)列表
 | ||||
|   getSimpleProductList() { | ||||
|     return request.get({ url: '/iot/product/list-all-simple' }) | ||||
|     return request.get({ url: '/iot/product/simple-list' }) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,127 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // IoT 数据桥梁 VO
 | ||||
| export interface DataBridgeVO { | ||||
|   id?: number // 桥梁编号
 | ||||
|   name?: string // 桥梁名称
 | ||||
|   description?: string // 桥梁描述
 | ||||
|   status?: number // 桥梁状态
 | ||||
|   direction?: number // 桥梁方向
 | ||||
|   type?: number // 桥梁类型
 | ||||
|   config?: | ||||
|     | HttpConfig | ||||
|     | MqttConfig | ||||
|     | RocketMQConfig | ||||
|     | KafkaMQConfig | ||||
|     | RabbitMQConfig | ||||
|     | RedisStreamMQConfig // 桥梁配置
 | ||||
| } | ||||
| 
 | ||||
| interface Config { | ||||
|   type: string | ||||
| } | ||||
| 
 | ||||
| /** HTTP 配置 */ | ||||
| export interface HttpConfig extends Config { | ||||
|   url: string | ||||
|   method: string | ||||
|   headers: Record<string, string> | ||||
|   query: Record<string, string> | ||||
|   body: string | ||||
| } | ||||
| 
 | ||||
| /** MQTT 配置 */ | ||||
| export interface MqttConfig extends Config { | ||||
|   url: string | ||||
|   username: string | ||||
|   password: string | ||||
|   clientId: string | ||||
|   topic: string | ||||
| } | ||||
| 
 | ||||
| /** RocketMQ 配置 */ | ||||
| export interface RocketMQConfig extends Config { | ||||
|   nameServer: string | ||||
|   accessKey: string | ||||
|   secretKey: string | ||||
|   group: string | ||||
|   topic: string | ||||
|   tags: string | ||||
| } | ||||
| 
 | ||||
| /** Kafka 配置 */ | ||||
| export interface KafkaMQConfig extends Config { | ||||
|   bootstrapServers: string | ||||
|   username: string | ||||
|   password: string | ||||
|   ssl: boolean | ||||
|   topic: string | ||||
| } | ||||
| 
 | ||||
| /** RabbitMQ 配置 */ | ||||
| export interface RabbitMQConfig extends Config { | ||||
|   host: string | ||||
|   port: number | ||||
|   virtualHost: string | ||||
|   username: string | ||||
|   password: string | ||||
|   exchange: string | ||||
|   routingKey: string | ||||
|   queue: string | ||||
| } | ||||
| 
 | ||||
| /** Redis Stream MQ 配置 */ | ||||
| export interface RedisStreamMQConfig extends Config { | ||||
|   host: string | ||||
|   port: number | ||||
|   password: string | ||||
|   database: number | ||||
|   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' | ||||
| } as const | ||||
| 
 | ||||
| // 数据桥梁 API
 | ||||
| export const DataBridgeApi = { | ||||
|   // 查询数据桥梁分页
 | ||||
|   getDataBridgePage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/data-bridge/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询数据桥梁详情
 | ||||
|   getDataBridge: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/data-bridge/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增数据桥梁
 | ||||
|   createDataBridge: async (data: DataBridgeVO) => { | ||||
|     return await request.post({ url: `/iot/data-bridge/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改数据桥梁
 | ||||
|   updateDataBridge: async (data: DataBridgeVO) => { | ||||
|     return await request.put({ url: `/iot/data-bridge/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除数据桥梁
 | ||||
|   deleteDataBridge: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/data-bridge/delete?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 导出数据桥梁 Excel
 | ||||
|   exportDataBridge: async (params) => { | ||||
|     return await request.download({ url: `/iot/data-bridge/export-excel`, params }) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,41 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| /** IoT 统计数据类型 */ | ||||
| export interface IotStatisticsSummaryRespVO { | ||||
|   productCategoryCount: number | ||||
|   productCount: number | ||||
|   deviceCount: number | ||||
|   deviceMessageCount: number | ||||
|   productCategoryTodayCount: number | ||||
|   productTodayCount: number | ||||
|   deviceTodayCount: number | ||||
|   deviceMessageTodayCount: number | ||||
|   deviceOnlineCount: number | ||||
|   deviceOfflineCount: number | ||||
|   deviceInactiveCount: number | ||||
|   productCategoryDeviceCounts: Record<string, number> | ||||
| } | ||||
| 
 | ||||
| /** IoT 消息统计数据类型 */ | ||||
| export interface IotStatisticsDeviceMessageSummaryRespVO { | ||||
|   upstreamCounts: Record<number, number> | ||||
|   downstreamCounts: Record<number, number> | ||||
| } | ||||
| 
 | ||||
| // IoT 数据统计 API
 | ||||
| export const ProductCategoryApi = { | ||||
|   // 查询基础的数据统计
 | ||||
|   getIotStatisticsSummary: 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`, | ||||
|       params | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| /** | ||||
|  * IoT 产品物模型 | ||||
|  */ | ||||
| export interface ThingModelData { | ||||
|   id?: number // 物模型功能编号
 | ||||
|   identifier?: string // 功能标识
 | ||||
|   name?: string // 功能名称
 | ||||
|   description?: string // 功能描述
 | ||||
|   productId?: number // 产品编号
 | ||||
|   productKey?: string // 产品标识
 | ||||
|   dataType: string // 数据类型,与 dataSpecs 的 dataType 保持一致
 | ||||
|   type: number // 功能类型
 | ||||
|   property: ThingModelProperty // 属性
 | ||||
|   event?: ThingModelEvent // 事件
 | ||||
|   service?: ThingModelService // 服务
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * IoT 模拟设备 | ||||
|  */ | ||||
| // TODO @super:和 ThingModelSimulatorData 会不会好点
 | ||||
| export interface SimulatorData extends ThingModelData { | ||||
|   simulateValue?: string | number // 用于存储模拟值 TODO @super:字段使用 value 会不会好点
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * ThingModelProperty 类型 | ||||
|  */ | ||||
| export interface ThingModelProperty { | ||||
|   [key: string]: any | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * ThingModelEvent 类型 | ||||
|  */ | ||||
| export interface ThingModelEvent { | ||||
|   [key: string]: any | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * ThingModelService 类型 | ||||
|  */ | ||||
| export interface ThingModelService { | ||||
|   [key: string]: any | ||||
| } | ||||
| 
 | ||||
| // IoT 产品物模型 API
 | ||||
| export const ThingModelApi = { | ||||
|   // 查询产品物模型分页
 | ||||
|   getThingModelPage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/thing-model/page`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获得产品物模型列表
 | ||||
|   getThingModelList: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/thing-model/list`, params }) | ||||
|   }, | ||||
| 
 | ||||
|   // 获得产品物模型
 | ||||
|   getThingModelListByProductId: async (params: any) => { | ||||
|     return await request.get({ | ||||
|       url: `/iot/thing-model/list-by-product-id`, | ||||
|       params | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询产品物模型详情
 | ||||
|   getThingModel: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/thing-model/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增产品物模型
 | ||||
|   createThingModel: async (data: ThingModelData) => { | ||||
|     return await request.post({ url: `/iot/thing-model/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改产品物模型
 | ||||
|   updateThingModel: async (data: ThingModelData) => { | ||||
|     return await request.put({ url: `/iot/thing-model/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除产品物模型
 | ||||
|   deleteThingModel: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/thing-model/delete?id=` + id }) | ||||
|   } | ||||
| } | ||||
|  | @ -1,55 +0,0 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // IoT 产品物模型 VO
 | ||||
| export interface ThinkModelFunctionVO { | ||||
|   id: number // 物模型功能编号
 | ||||
|   identifier: string // 功能标识
 | ||||
|   name: string // 功能名称
 | ||||
|   description: string // 功能描述
 | ||||
|   productId: number // 产品编号
 | ||||
|   productKey: string // 产品标识
 | ||||
|   type: number // 功能类型
 | ||||
|   property: string // 属性
 | ||||
|   event: string // 事件
 | ||||
|   service: string // 服务
 | ||||
| } | ||||
| 
 | ||||
| // IoT 产品物模型 API
 | ||||
| export const ThinkModelFunctionApi = { | ||||
|   // 查询产品物模型分页
 | ||||
|   getThinkModelFunctionPage: async (params: any) => { | ||||
|     return await request.get({ url: `/iot/think-model-function/page`, params }) | ||||
|   }, | ||||
|   // 获得产品物模型
 | ||||
|   getThinkModelFunctionListByProductId: async (params: any) => { | ||||
|     return await request.get({ | ||||
|       url: `/iot/think-model-function/list-by-product-id`, | ||||
|       params | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|   // 查询产品物模型详情
 | ||||
|   getThinkModelFunction: async (id: number) => { | ||||
|     return await request.get({ url: `/iot/think-model-function/get?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 新增产品物模型
 | ||||
|   createThinkModelFunction: async (data: ThinkModelFunctionVO) => { | ||||
|     return await request.post({ url: `/iot/think-model-function/create`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 修改产品物模型
 | ||||
|   updateThinkModelFunction: async (data: ThinkModelFunctionVO) => { | ||||
|     return await request.put({ url: `/iot/think-model-function/update`, data }) | ||||
|   }, | ||||
| 
 | ||||
|   // 删除产品物模型
 | ||||
|   deleteThinkModelFunction: async (id: number) => { | ||||
|     return await request.delete({ url: `/iot/think-model-function/delete?id=` + id }) | ||||
|   }, | ||||
| 
 | ||||
|   // 导出产品物模型 Excel
 | ||||
|   exportThinkModelFunction: async (params) => { | ||||
|     return await request.download({ url: `/iot/think-model-function/export-excel`, params }) | ||||
|   } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 9.8 KiB | 
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" class="design-iconfont" viewBox="0 0 12 12"><path fill="url(#a)" fill-rule="evenodd" d="M1 0a1 1 0 0 0-1 1v3.538a1 1 0 0 0 1 1h3.538a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1H1Zm0 6.462a1 1 0 0 0-1 1V11a1 1 0 0 0 1 1h3.538a1 1 0 0 0 1-1V7.462a1 1 0 0 0-1-1H1ZM6.462 1a1 1 0 0 1 1-1H11a1 1 0 0 1 1 1v3.538a1 1 0 0 1-1 1H7.462a1 1 0 0 1-1-1V1Zm1 5.462a1 1 0 0 0-1 1V11a1 1 0 0 0 1 1H11a1 1 0 0 0 1-1V7.462a1 1 0 0 0-1-1H7.462Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="0" x2="12" y1="0" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient></defs></svg> | ||||
| After Width: | Height: | Size: 697 B | 
|  | @ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 12 12"><g clip-path="url(#a)"><path fill="url(#b)" fill-rule="evenodd" d="M6.958.42C6.444.216 5.61.216 5.098.42L1.15 1.975c-.77.304-.77.797 0 1.1l3.947 1.558c.514.202 1.347.202 1.86 0l3.948-1.557c.77-.304.77-.797 0-1.1L6.958.418ZM4.715 11.788a.857.857 0 0 0 .3.056c.383 0 .671-.295.671-.7V6.404c0-.49-.364-1.007-.817-1.177L1.09 3.805a.808.808 0 0 0-.284-.056c-.353 0-.581.275-.581.7V9.19c0 .508.33 1.014.763 1.177l3.726 1.422Zm2.229-.024h-.02l.073.003c.074.004.154.009.227-.019L11 10.367c.45-.168.83-.686.83-1.177V4.45c0-.413-.29-.7-.673-.7a.965.965 0 0 0-.317.055l-3.72 1.422c-.44.165-.75.67-.75 1.177v4.74c0 .42.218.621.575.621Z" clip-rule="evenodd"/></g><defs><linearGradient id="b" x1=".226" x2="11.803" y1=".267" y2="11.871" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h12v12H0z"/></clipPath></defs></svg> | ||||
| After Width: | Height: | Size: 1011 B | 
|  | @ -56,7 +56,7 @@ export default defineComponent({ | |||
|     // 注册 | ||||
|     onMounted(() => { | ||||
|       const tableRef = unref(elTableRef) | ||||
|       emit('register', tableRef?.$parent, elTableRef) | ||||
|       emit('register', tableRef?.$parent, elTableRef.value) | ||||
|     }) | ||||
| 
 | ||||
|     const pageSizeRef = ref(props.pageSize) | ||||
|  |  | |||
|  | @ -689,15 +689,15 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|     }, | ||||
|     children: [ | ||||
|       { | ||||
|         path: 'product/detail/:id', | ||||
|         path: 'product/product/detail/:id', | ||||
|         name: 'IoTProductDetail', | ||||
|         meta: { | ||||
|           title: '产品详情', | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|           activeMenu: '/iot/product' | ||||
|           activeMenu: '/iot/device/product' | ||||
|         }, | ||||
|         component: () => import('@/views/iot/product/detail/index.vue') | ||||
|         component: () => import('@/views/iot/product/product/detail/index.vue') | ||||
|       }, | ||||
|       { | ||||
|         path: 'device/detail/:id', | ||||
|  | @ -706,9 +706,20 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|           title: '设备详情', | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|           activeMenu: '/iot/device' | ||||
|           activeMenu: '/iot/device/device' | ||||
|         }, | ||||
|         component: () => import('@/views/iot/device/detail/index.vue') | ||||
|         component: () => import('@/views/iot/device/device/detail/index.vue') | ||||
|       }, | ||||
|       { | ||||
|         path: 'plugin/detail/:id', | ||||
|         name: 'IoTPluginDetail', | ||||
|         meta: { | ||||
|           title: '插件详情', | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|           activeMenu: '/iot/plugin' | ||||
|         }, | ||||
|         component: () => import('@/views/iot/plugin/detail/index.vue') | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
|  |  | |||
|  | @ -236,9 +236,14 @@ export enum DICT_TYPE { | |||
|   IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
 | ||||
|   IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
 | ||||
|   IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
 | ||||
|   IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
 | ||||
|   IOT_PRODUCT_FUNCTION_TYPE = 'iot_product_function_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_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
 | ||||
|   IOT_RW_TYPE = 'iot_rw_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' // 桥梁类型
 | ||||
| } | ||||
|  |  | |||
|  | @ -116,9 +116,23 @@ export function toAnyString() { | |||
|   return str | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 生成指定长度的随机字符串 | ||||
|  * | ||||
|  * @param length 字符串长度 | ||||
|  */ | ||||
| export function generateRandomStr(length: number): string { | ||||
|   const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' | ||||
|   let result = '' | ||||
|   for (let i = 0; i < length; i++) { | ||||
|     result += chars.charAt(Math.floor(Math.random() * chars.length)) | ||||
|   } | ||||
|   return result | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 根据支持的文件类型生成 accept 属性值 | ||||
|  *  | ||||
|  * | ||||
|  * @param supportedFileTypes 支持的文件类型数组,如 ['PDF', 'DOC', 'DOCX'] | ||||
|  * @returns 用于文件上传组件 accept 属性的字符串 | ||||
|  */ | ||||
|  | @ -503,7 +517,7 @@ export function jsonParse(str: string) { | |||
|   try { | ||||
|     return JSON.parse(str) | ||||
|   } catch (e) { | ||||
|     console.error(`str[${str}] 不是一个 JSON 字符串`) | ||||
|     console.log(`str[${str}] 不是一个 JSON 字符串`) | ||||
|     return '' | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,156 +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="productId"> | ||||
|         <el-select | ||||
|           v-model="formData.productId" | ||||
|           placeholder="请选择产品" | ||||
|           :disabled="formType === 'update'" | ||||
|           clearable | ||||
|         > | ||||
|           <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="formData.deviceName" | ||||
|           placeholder="请输入 DeviceName" | ||||
|           :disabled="formType === 'update'" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="备注名称" prop="nickname"> | ||||
|         <el-input v-model="formData.nickname" placeholder="请输入备注名称" /> | ||||
|       </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 { DeviceApi, DeviceVO } from '@/api/iot/device' | ||||
| import { ProductApi } from '@/api/iot/product' | ||||
| 
 | ||||
| /** IoT 设备 表单 */ | ||||
| defineOptions({ name: 'IoTDeviceForm' }) | ||||
| 
 | ||||
| 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, | ||||
|   productId: undefined, | ||||
|   deviceName: undefined, | ||||
|   nickname: undefined | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }], | ||||
|   deviceName: [ | ||||
|     { | ||||
|       pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/, | ||||
|       message: | ||||
|         '支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@,长度限制为 4~32 个字符', | ||||
|       trigger: 'blur' | ||||
|     } | ||||
|   ], | ||||
|   nickname: [ | ||||
|     { | ||||
|       validator: (rule, value, callback) => { | ||||
|         if (value === undefined || value === null) { | ||||
|           callback() | ||||
|           return | ||||
|         } | ||||
|         const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length | ||||
|         if (length < 4 || length > 64) { | ||||
|           callback(new Error('备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符')) | ||||
|         } else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) { | ||||
|           callback(new Error('备注名称只能包含中文、英文字母、日文、数字和下划线(_)')) | ||||
|         } else { | ||||
|           callback() | ||||
|         } | ||||
|       }, | ||||
|       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 DeviceApi.getDevice(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 DeviceVO | ||||
|     if (formType.value === 'create') { | ||||
|       await DeviceApi.createDevice(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await DeviceApi.updateDevice(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     productId: undefined, | ||||
|     deviceName: undefined, | ||||
|     nickname: undefined | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| 
 | ||||
| /** 查询字典下拉列表 */ | ||||
| const products = ref() | ||||
| const getProducts = async () => { | ||||
|   products.value = await ProductApi.getSimpleProductList() | ||||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   getProducts() | ||||
| }) | ||||
| </script> | ||||
|  | @ -1,123 +0,0 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <el-collapse v-model="activeNames"> | ||||
|       <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="激活时间"> | ||||
|           {{ formatDate(device.activeTime) }} | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item label="最后上线时间"> | ||||
|           {{ formatDate(device.lastOnlineTime) }} | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item label="当前状态"> | ||||
|           <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.status" /> | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item label="最后离线时间" :span="3"> | ||||
|           {{ formatDate(device.lastOfflineTime) }} | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item label="MQTT 连接参数"> | ||||
|           <el-button type="primary" @click="openMqttParams">查看</el-button> | ||||
|         </el-descriptions-item> | ||||
|       </el-descriptions> | ||||
|     </el-collapse> | ||||
| 
 | ||||
|     <!-- 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="password"> | ||||
|             <template #append> | ||||
|               <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> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue' | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { ProductVO } from '@/api/iot/product' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| import { DeviceVO } from '@/api/iot/device' | ||||
| 
 | ||||
| const message = useMessage() // 消息提示 | ||||
| 
 | ||||
| const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props | ||||
| 
 | ||||
| const emit = defineEmits(['refresh']) // 定义 Emits | ||||
| 
 | ||||
| const activeNames = ref(['basicInfo']) // 展示的折叠面板 | ||||
| const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性 | ||||
| const mqttParams = ref({ | ||||
|   mqttClientId: '', | ||||
|   mqttUsername: '', | ||||
|   mqttPassword: '' | ||||
| }) // 定义 MQTT 参数对象 | ||||
| 
 | ||||
| /** 复制到剪贴板方法 */ | ||||
| const copyToClipboard = (text: string) => { | ||||
|   navigator.clipboard.writeText(text).then(() => { | ||||
|     message.success('复制成功') | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 打开 MQTT 参数弹框的方法 */ | ||||
| const openMqttParams = () => { | ||||
|   mqttParams.value = { | ||||
|     mqttClientId: device.mqttClientId || 'N/A', | ||||
|     mqttUsername: device.mqttUsername || 'N/A', | ||||
|     mqttPassword: device.mqttPassword || 'N/A' | ||||
|   } | ||||
|   mqttDialogVisible.value = true | ||||
| } | ||||
| 
 | ||||
| /** 关闭 MQTT 弹框的方法 */ | ||||
| const handleCloseMqttDialog = () => { | ||||
|   mqttDialogVisible.value = false | ||||
| } | ||||
| </script> | ||||
|  | @ -1,66 +0,0 @@ | |||
| <template> | ||||
|   <DeviceDetailsHeader | ||||
|     :loading="loading" | ||||
|     :product="product" | ||||
|     :device="device" | ||||
|     @refresh="getDeviceData(id)" | ||||
|   /> | ||||
|   <el-col> | ||||
|     <el-tabs> | ||||
|       <el-tab-pane label="设备信息"> | ||||
|         <DeviceDetailsInfo :product="product" :device="device" /> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane label="Topic 列表" /> | ||||
|       <el-tab-pane label="物模型数据" /> | ||||
|       <el-tab-pane label="子设备管理" /> | ||||
|     </el-tabs> | ||||
|   </el-col> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
| import { DeviceApi, DeviceVO } from '@/api/iot/device' | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product' | ||||
| import DeviceDetailsHeader from '@/views/iot/device/detail/DeviceDetailsHeader.vue' | ||||
| import DeviceDetailsInfo from '@/views/iot/device/detail/DeviceDetailsInfo.vue' | ||||
| 
 | ||||
| defineOptions({ name: 'IoTDeviceDetail' }) | ||||
| 
 | ||||
| const route = useRoute() | ||||
| const message = useMessage() | ||||
| const id = route.params.id // 编号 | ||||
| const loading = ref(true) // 加载中 | ||||
| const product = ref<ProductVO>({} as ProductVO) // 产品详情 | ||||
| const device = ref<DeviceVO>({} as DeviceVO) // 设备详情 | ||||
| 
 | ||||
| /** 获取设备详情 */ | ||||
| const getDeviceData = async (id: number) => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     device.value = await DeviceApi.getDevice(id) | ||||
|     console.log(product.value) | ||||
|     await getProductData(device.value.productId) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 获取产品详情 */ | ||||
| const getProductData = async (id: number) => { | ||||
|   product.value = await ProductApi.getProduct(id) | ||||
|   console.log(product.value) | ||||
| } | ||||
| 
 | ||||
| /** 获取物模型 */ | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| const { delView } = useTagsViewStore() // 视图操作 | ||||
| const { currentRoute } = useRouter() // 路由 | ||||
| onMounted(async () => { | ||||
|   if (!id) { | ||||
|     message.warning('参数错误,产品不能为空!') | ||||
|     delView(unref(currentRoute)) | ||||
|     return | ||||
|   } | ||||
|   await getDeviceData(id) | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,263 @@ | |||
| <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="productId"> | ||||
|         <el-select | ||||
|           v-model="formData.productId" | ||||
|           placeholder="请选择产品" | ||||
|           :disabled="formType === 'update'" | ||||
|           clearable | ||||
|           @change="handleProductChange" | ||||
|         > | ||||
|           <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="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" | ||||
|           placeholder="请输入 DeviceName" | ||||
|           :disabled="formType === 'update'" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item | ||||
|         v-if="formData.deviceType === DeviceTypeEnum.GATEWAY_SUB" | ||||
|         label="网关设备" | ||||
|         prop="gatewayId" | ||||
|       > | ||||
|         <el-select v-model="formData.gatewayId" placeholder="子设备可选择父设备" clearable> | ||||
|           <el-option | ||||
|             v-for="gateway in gatewayDevices" | ||||
|             :key="gateway.id" | ||||
|             :label="gateway.nickname || gateway.deviceName" | ||||
|             :value="gateway.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-collapse> | ||||
|         <el-collapse-item title="更多配置"> | ||||
|           <el-form-item label="备注名称" prop="nickname"> | ||||
|             <el-input v-model="formData.nickname" placeholder="请输入备注名称" /> | ||||
|           </el-form-item> | ||||
|           <el-form-item label="设备图片" prop="picUrl"> | ||||
|             <UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" /> | ||||
|           </el-form-item> | ||||
|           <el-form-item label="设备分组" prop="groupIds"> | ||||
|             <el-select v-model="formData.groupIds" placeholder="请选择设备分组" multiple clearable> | ||||
|               <el-option | ||||
|                 v-for="group in deviceGroups" | ||||
|                 :key="group.id" | ||||
|                 :label="group.name" | ||||
|                 :value="group.id" | ||||
|               /> | ||||
|             </el-select> | ||||
|           </el-form-item> | ||||
|           <el-form-item label="设备序列号" prop="serialNumber"> | ||||
|             <el-input v-model="formData.serialNumber" placeholder="请输入设备序列号" /> | ||||
|           </el-form-item> | ||||
|         </el-collapse-item> | ||||
|       </el-collapse> | ||||
|     </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 { DeviceApi, DeviceVO } from '@/api/iot/device/device' | ||||
| import { DeviceGroupApi } from '@/api/iot/device/group' | ||||
| import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product' | ||||
| import { UploadImg } from '@/components/UploadFile' | ||||
| import { generateRandomStr } from '@/utils' | ||||
| 
 | ||||
| /** IoT 设备表单 */ | ||||
| defineOptions({ name: 'IoTDeviceForm' }) | ||||
| 
 | ||||
| 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, | ||||
|   productId: undefined, | ||||
|   deviceKey: undefined as string | undefined, | ||||
|   deviceName: undefined, | ||||
|   nickname: undefined, | ||||
|   picUrl: undefined, | ||||
|   gatewayId: undefined, | ||||
|   deviceType: undefined as number | undefined, | ||||
|   serialNumber: undefined, | ||||
|   groupIds: [] as number[] | ||||
| }) | ||||
| 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' }, | ||||
|     { | ||||
|       pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/, | ||||
|       message: | ||||
|         '支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@,长度限制为 4~32 个字符', | ||||
|       trigger: 'blur' | ||||
|     } | ||||
|   ], | ||||
|   nickname: [ | ||||
|     { | ||||
|       validator: (rule, value, callback) => { | ||||
|         if (value === undefined || value === null) { | ||||
|           callback() | ||||
|           return | ||||
|         } | ||||
|         const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length | ||||
|         if (length < 4 || length > 64) { | ||||
|           callback(new Error('备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符')) | ||||
|         } else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) { | ||||
|           callback(new Error('备注名称只能包含中文、英文字母、日文、数字和下划线(_)')) | ||||
|         } else { | ||||
|           callback() | ||||
|         } | ||||
|       }, | ||||
|       trigger: 'blur' | ||||
|     } | ||||
|   ], | ||||
|   serialNumber: [ | ||||
|     { | ||||
|       pattern: /^[a-zA-Z0-9-_]+$/, | ||||
|       message: '序列号只能包含字母、数字、中划线和下划线', | ||||
|       trigger: 'blur' | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
| const formRef = ref() // 表单 Ref | ||||
| const products = ref<ProductVO[]>([]) // 产品列表 | ||||
| const gatewayDevices = ref<DeviceVO[]>([]) // 网关设备列表 | ||||
| const deviceGroups = ref<any[]>([]) | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| 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 DeviceApi.getDevice(id) | ||||
|     } finally { | ||||
|       formLoading.value = false | ||||
|     } | ||||
|   } else { | ||||
|     generateDeviceKey() | ||||
|   } | ||||
| 
 | ||||
|   // 加载网关设备列表 | ||||
|   try { | ||||
|     gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY) | ||||
|   } catch (error) { | ||||
|     console.error('加载网关设备列表失败:', error) | ||||
|   } | ||||
|   // 加载产品列表 | ||||
|   products.value = await ProductApi.getSimpleProductList() | ||||
| 
 | ||||
|   // 加载设备分组列表 | ||||
|   try { | ||||
|     deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList() | ||||
|   } catch (error) { | ||||
|     console.error('加载设备分组列表失败:', error) | ||||
|   } | ||||
| } | ||||
| 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 DeviceVO | ||||
|     if (formType.value === 'create') { | ||||
|       await DeviceApi.createDevice(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await DeviceApi.updateDevice(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     productId: undefined, | ||||
|     deviceKey: undefined, | ||||
|     deviceName: undefined, | ||||
|     nickname: undefined, | ||||
|     picUrl: undefined, | ||||
|     gatewayId: undefined, | ||||
|     deviceType: undefined, | ||||
|     serialNumber: undefined, | ||||
|     groupIds: [] | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| 
 | ||||
| /** 产品选择变化 */ | ||||
| const handleProductChange = (productId: number) => { | ||||
|   if (!productId) { | ||||
|     formData.value.deviceType = undefined | ||||
|     return | ||||
|   } | ||||
|   const product = products.value?.find((item) => item.id === productId) | ||||
|   formData.value.deviceType = product?.deviceType | ||||
| } | ||||
| 
 | ||||
| /** 生成 DeviceKey */ | ||||
| const generateDeviceKey = () => { | ||||
|   formData.value.deviceKey = generateRandomStr(16) | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,90 @@ | |||
| <template> | ||||
|   <Dialog :title="'添加设备到分组'" v-model="dialogVisible"> | ||||
|     <el-form | ||||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="formRules" | ||||
|       label-width="100px" | ||||
|       v-loading="formLoading" | ||||
|     > | ||||
|       <el-form-item label="设备分组" prop="groupIds"> | ||||
|         <el-select v-model="formData.groupIds" placeholder="请选择设备分组" multiple clearable> | ||||
|           <el-option | ||||
|             v-for="group in deviceGroups" | ||||
|             :key="group.id" | ||||
|             :label="group.name" | ||||
|             :value="group.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> | ||||
|   </Dialog> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { DeviceApi } from '@/api/iot/device/device' | ||||
| import { DeviceGroupApi } from '@/api/iot/device/group' | ||||
| 
 | ||||
| defineOptions({ name: 'IoTDeviceGroupForm' }) | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息窗 | ||||
| 
 | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const formLoading = ref(false) // 表单的加载中 | ||||
| const formData = ref({ | ||||
|   ids: [] as number[], | ||||
|   groupIds: [] as number[] | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   groupIds: [{ required: true, message: '设备分组不能为空', trigger: 'change' }] | ||||
| }) | ||||
| const formRef = ref() // 表单 Ref | ||||
| const deviceGroups = ref<any[]>([]) // 设备分组列表 | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async (ids: number[]) => { | ||||
|   dialogVisible.value = true | ||||
|   resetForm() | ||||
|   formData.value.ids = ids | ||||
| 
 | ||||
|   // 加载设备分组列表 | ||||
|   try { | ||||
|     deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList() | ||||
|   } catch (error) { | ||||
|     console.error('加载设备分组列表失败:', error) | ||||
|   } | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| const submitForm = async () => { | ||||
|   // 校验表单 | ||||
|   await formRef.value.validate() | ||||
|   // 提交请求 | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     await DeviceApi.updateDeviceGroup(formData.value) | ||||
|     message.success(t('common.updateSuccess')) | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     ids: [], | ||||
|     groupIds: [] | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,139 @@ | |||
| <template> | ||||
|   <Dialog v-model="dialogVisible" title="设备导入" width="400"> | ||||
|     <el-upload | ||||
|       ref="uploadRef" | ||||
|       v-model:file-list="fileList" | ||||
|       :action="importUrl + '?updateSupport=' + updateSupport" | ||||
|       :auto-upload="false" | ||||
|       :disabled="formLoading" | ||||
|       :headers="uploadHeaders" | ||||
|       :limit="1" | ||||
|       :on-error="submitFormError" | ||||
|       :on-exceed="handleExceed" | ||||
|       :on-success="submitFormSuccess" | ||||
|       accept=".xlsx, .xls" | ||||
|       drag | ||||
|     > | ||||
|       <Icon icon="ep:upload" /> | ||||
|       <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> | ||||
|       <template #tip> | ||||
|         <div class="el-upload__tip text-center"> | ||||
|           <div class="el-upload__tip"> | ||||
|             <el-checkbox v-model="updateSupport" /> | ||||
|             是否更新已经存在的设备数据 | ||||
|           </div> | ||||
|           <span>仅允许导入 xls、xlsx 格式文件。</span> | ||||
|           <el-link | ||||
|             :underline="false" | ||||
|             style="font-size: 12px; vertical-align: baseline" | ||||
|             type="primary" | ||||
|             @click="importTemplate" | ||||
|           > | ||||
|             下载模板 | ||||
|           </el-link> | ||||
|         </div> | ||||
|       </template> | ||||
|     </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 { DeviceApi } from '@/api/iot/device/device' | ||||
| import { getAccessToken, getTenantId } from '@/utils/auth' | ||||
| import download from '@/utils/download' | ||||
| 
 | ||||
| defineOptions({ name: 'IoTDeviceImportForm' }) | ||||
| 
 | ||||
| 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/device/import' | ||||
| const uploadHeaders = ref() // 上传 Header 头 | ||||
| const fileList = ref([]) // 文件列表 | ||||
| const updateSupport = ref(0) // 是否更新已经存在的设备数据 | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = () => { | ||||
|   dialogVisible.value = true | ||||
|   updateSupport.value = 0 | ||||
|   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 | ||||
|   } | ||||
|   // 拼接提示语 | ||||
|   const data = response.data | ||||
|   let text = '上传成功数量:' + data.createDeviceNames.length + ';' | ||||
|   for (let deviceName of data.createDeviceNames) { | ||||
|     text += '< ' + deviceName + ' >' | ||||
|   } | ||||
|   text += '更新成功数量:' + data.updateDeviceNames.length + ';' | ||||
|   for (const deviceName of data.updateDeviceNames) { | ||||
|     text += '< ' + deviceName + ' >' | ||||
|   } | ||||
|   text += '更新失败数量:' + Object.keys(data.failureDeviceNames).length + ';' | ||||
|   for (const deviceName in data.failureDeviceNames) { | ||||
|     text += '< ' + deviceName + ': ' + data.failureDeviceNames[deviceName] + ' >' | ||||
|   } | ||||
|   message.alert(text) | ||||
|   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('最多只能上传一个文件!') | ||||
| } | ||||
| 
 | ||||
| /** 下载模板操作 */ | ||||
| const importTemplate = async () => { | ||||
|   const res = await DeviceApi.importDeviceTemplate() | ||||
|   download.excel(res, '设备导入模版.xls') | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,110 @@ | |||
| <!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)--> | ||||
| <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> | ||||
|  | @ -0,0 +1,119 @@ | |||
| <!-- 设备配置 --> | ||||
| <template> | ||||
|   <div> | ||||
|     <el-alert | ||||
|       title="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。" | ||||
|       type="info" | ||||
|       show-icon | ||||
|       class="my-4" | ||||
|       description="如需编辑文件,请点击下方编辑按钮" | ||||
|     /> | ||||
| 
 | ||||
|     <!-- JSON 编辑器:读模式 --> | ||||
|     <Vue3Jsoneditor | ||||
|       v-if="isEditing" | ||||
|       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" | ||||
|       @error="onError" | ||||
|     /> | ||||
|     <div class="mt-5 text-center"> | ||||
|       <el-button v-if="isEditing" @click="cancelEdit">取消</el-button> | ||||
|       <el-button v-if="isEditing" type="primary" @click="saveConfig" :disabled="hasJsonError"> | ||||
|         保存 | ||||
|       </el-button> | ||||
|       <el-button v-else @click="enableEdit">编辑</el-button> | ||||
|       <!-- TODO @芋艿:缺一个下发按钮 --> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import Vue3Jsoneditor from 'v3-jsoneditor/src/Vue3Jsoneditor.vue' | ||||
| import { DeviceApi, DeviceVO } from '@/api/iot/device/device' | ||||
| import { jsonParse } from '@/utils' | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   device: DeviceVO | ||||
| }>() | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'success'): void // 定义 success 事件,不需要参数 | ||||
| }>() | ||||
| 
 | ||||
| const message = useMessage() | ||||
| const loading = ref(false) // 加载中 | ||||
| const config = ref<any>({}) // 只存储 config 字段 | ||||
| const hasJsonError = ref(false) // 是否有 JSON 格式错误 | ||||
| 
 | ||||
| /** 监听 props.device 的变化,只更新 config 字段 */ | ||||
| watchEffect(() => { | ||||
|   config.value = jsonParse(props.device.config) | ||||
| }) | ||||
| 
 | ||||
| const isEditing = ref(false) // 编辑状态 | ||||
| const editorOptions = computed(() => ({ | ||||
|   mainMenuBar: false, | ||||
|   navigationBar: false, | ||||
|   statusBar: false | ||||
| })) // JSON 编辑器的选项 | ||||
| 
 | ||||
| /** 启用编辑模式的函数 */ | ||||
| const enableEdit = () => { | ||||
|   isEditing.value = true | ||||
|   hasJsonError.value = false // 重置错误状态 | ||||
| } | ||||
| 
 | ||||
| /** 取消编辑的函数 */ | ||||
| const cancelEdit = () => { | ||||
|   config.value = jsonParse(props.device.config) | ||||
|   isEditing.value = false | ||||
|   hasJsonError.value = false // 重置错误状态 | ||||
| } | ||||
| 
 | ||||
| /** 保存配置的函数 */ | ||||
| const saveConfig = async () => { | ||||
|   if (hasJsonError.value) { | ||||
|     message.error('JSON格式错误,请修正后再提交!') | ||||
|     return | ||||
|   } | ||||
|   await updateDeviceConfig() | ||||
|   isEditing.value = false | ||||
| } | ||||
| 
 | ||||
| /** 更新设备配置 */ | ||||
| const updateDeviceConfig = async () => { | ||||
|   try { | ||||
|     // 提交请求 | ||||
|     loading.value = true | ||||
|     await DeviceApi.updateDevice({ | ||||
|       id: props.device.id, | ||||
|       config: JSON.stringify(config.value) | ||||
|     } as DeviceVO) | ||||
|     message.success('更新成功!') | ||||
|     // 触发 success 事件 | ||||
|     emit('success') | ||||
|   } catch (error) { | ||||
|     console.error(error) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 处理 JSON 编辑器错误的函数 */ | ||||
| const onError = (e: any) => { | ||||
|   console.log('onError', e) | ||||
|   hasJsonError.value = true | ||||
| } | ||||
| </script> | ||||
|  | @ -1,3 +1,4 @@ | |||
| <!-- 设备信息(头部) --> | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="flex items-start justify-between"> | ||||
|  | @ -35,41 +36,33 @@ | |||
|   <DeviceForm ref="formRef" @success="emit('refresh')" /> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue' | ||||
| import DeviceForm from '@/views/iot/device/DeviceForm.vue' | ||||
| import { ProductVO } from '@/api/iot/product' | ||||
| import { DeviceVO } from '@/api/iot/device' | ||||
| import { useRouter } from 'vue-router' | ||||
| import DeviceForm from '@/views/iot/device/device/DeviceForm.vue' | ||||
| import { ProductVO } from '@/api/iot/product/product' | ||||
| import { DeviceVO } from '@/api/iot/device/device' | ||||
| 
 | ||||
| const message = useMessage() | ||||
| const router = useRouter() | ||||
| 
 | ||||
| // 操作修改 | ||||
| const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() | ||||
| const emit = defineEmits(['refresh']) | ||||
| 
 | ||||
| /** 操作修改 */ | ||||
| const formRef = ref() | ||||
| const openForm = (type: string, id?: number) => { | ||||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() | ||||
| const emit = defineEmits(['refresh']) | ||||
| 
 | ||||
| /** | ||||
|  * 将文本复制到剪贴板 | ||||
|  * | ||||
|  * @param text 需要复制的文本 | ||||
|  */ | ||||
| const copyToClipboard = (text: string) => { | ||||
|   // TODO @haohao:可以考虑用 await 异步转同步哈 | ||||
|   navigator.clipboard.writeText(text).then(() => { | ||||
| /** 复制到剪贴板方法 */ | ||||
| const copyToClipboard = async (text: string) => { | ||||
|   try { | ||||
|     await navigator.clipboard.writeText(text) | ||||
|     message.success('复制成功') | ||||
|   }) | ||||
|   } catch (error) { | ||||
|     message.error('复制失败') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 跳转到产品详情页面 | ||||
|  * | ||||
|  * @param productId 产品 ID | ||||
|  */ | ||||
| /** 跳转到产品详情页面 */ | ||||
| const goToProductDetail = (productId: number) => { | ||||
|   router.push({ name: 'IoTProductDetail', params: { id: productId } }) | ||||
| } | ||||
|  | @ -0,0 +1,144 @@ | |||
| <!-- 设备信息 --> | ||||
| <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> | ||||
| 
 | ||||
|   <!-- 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> | ||||
| 
 | ||||
|   <!-- 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' | ||||
| 
 | ||||
| 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 copyToClipboard = async (text: string) => { | ||||
|   try { | ||||
|     await navigator.clipboard.writeText(text) | ||||
|     message.success('复制成功') | ||||
|   } catch (error) { | ||||
|     message.error('复制失败') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 打开 MQTT 参数弹框的方法 */ | ||||
| const openMqttParams = 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 | ||||
|   } catch (error) { | ||||
|     console.error('获取 MQTT 连接参数出错:', error) | ||||
|     message.error('获取MQTT连接参数失败,请检查网络连接或联系管理员') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 关闭 MQTT 弹框的方法 */ | ||||
| const handleCloseMqttDialog = () => { | ||||
|   mqttDialogVisible.value = false | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,166 @@ | |||
| <!-- 设备日志 --> | ||||
| <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> | ||||
|  | @ -0,0 +1,134 @@ | |||
| <!-- 设备物模型:运行状态(属性)、事件管理、服务调用 --> | ||||
| <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> | ||||
|  | @ -0,0 +1,331 @@ | |||
| <!-- 模拟设备 --> | ||||
| <template> | ||||
|   <ContentWrap> | ||||
|     <el-row :gutter="20"> | ||||
|       <!-- 左侧指令调试区域 --> | ||||
|       <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="property"> | ||||
|                 <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:不用翻译,可以减少宽度的占用 --> | ||||
|                       <template #default="{ row }"> | ||||
|                         {{ dataTypeOptionsLabel(row.property?.dataType) ?? '-' }} | ||||
|                       </template> | ||||
|                     </el-table-column> | ||||
|                     <el-table-column align="left" label="数据定义" prop="identifier"> | ||||
|                       <template #default="{ row }"> | ||||
|                         <DataDefinition :data="row" /> | ||||
|                       </template> | ||||
|                     </el-table-column> | ||||
|                     <!-- TODO @super:可以右侧 fixed --> | ||||
|                     <el-table-column align="center" label="值" width="80"> | ||||
|                       <template #default="scope"> | ||||
|                         <el-input v-model="scope.row.simulateValue" class="!w-60px" /> | ||||
|                       </template> | ||||
|                     </el-table-column> | ||||
|                   </el-table> | ||||
|                   <!-- TODO @super:发送按钮,可以放在右侧哈。因为我们的 simulateValue 就在最右侧 --> | ||||
|                   <div class="mt-10px"> | ||||
|                     <el-button type="primary" @click="handlePropertyReport"> 发送</el-button> | ||||
|                   </div> | ||||
|                 </ContentWrap> | ||||
|               </el-tab-pane> | ||||
| 
 | ||||
|               <!-- 事件上报 --> | ||||
|               <!-- TODO @super:待实现 --> | ||||
|               <el-tab-pane label="事件上报" name="event"> | ||||
|                 <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-column | ||||
|                       label="数据定义" | ||||
|                       align="center" | ||||
|                       prop="specs" | ||||
|                       :show-overflow-tooltip="true" | ||||
|                     /> | ||||
|                     <el-table-column label="值" align="center" width="80"> | ||||
|                       <template #default="scope"> | ||||
|                         <el-input v-model="scope.row.simulateValue" class="!w-60px" /> | ||||
|                       </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"> | ||||
|                 <ContentWrap> | ||||
|                   <div class="flex gap-4"> | ||||
|                     <el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)"> | ||||
|                       设备上线 | ||||
|                     </el-button> | ||||
|                     <el-button type="danger" @click="handleDeviceState(DeviceStateEnum.OFFLINE)"> | ||||
|                       设备下线 | ||||
|                     </el-button> | ||||
|                   </div> | ||||
|                 </ContentWrap> | ||||
|               </el-tab-pane> | ||||
|             </el-tabs> | ||||
|           </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="propertyDebug"> | ||||
|                 <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-column | ||||
|                       label="数据定义" | ||||
|                       align="center" | ||||
|                       prop="specs" | ||||
|                       :show-overflow-tooltip="true" | ||||
|                     /> | ||||
|                     <el-table-column label="值" align="center" width="80"> | ||||
|                       <template #default="scope"> | ||||
|                         <el-input v-model="scope.row.simulateValue" class="!w-60px" /> | ||||
|                       </template> | ||||
|                     </el-table-column> | ||||
|                   </el-table> | ||||
|                   <div class="mt-10px"> | ||||
|                     <el-button type="primary" @click="handlePropertyGet">获取</el-button> | ||||
|                   </div> --> | ||||
|                 </ContentWrap> | ||||
|               </el-tab-pane> | ||||
| 
 | ||||
|               <!-- 服务调用 --> | ||||
|               <!-- TODO @super:待实现 --> | ||||
|               <el-tab-pane label="服务调用" name="service"> | ||||
|                 <ContentWrap> | ||||
|                   <!-- 服务调用相关内容 --> | ||||
|                 </ContentWrap> | ||||
|               </el-tab-pane> | ||||
|             </el-tabs> | ||||
|           </el-tab-pane> | ||||
|         </el-tabs> | ||||
|       </el-col> | ||||
| 
 | ||||
|       <!-- 右侧设备日志区域 --> | ||||
|       <el-col :span="12"> | ||||
|         <el-tabs type="border-card"> | ||||
|           <el-tab-pane label="设备日志"> | ||||
|             <DeviceDetailsLog :device-key="device.deviceKey" /> | ||||
|           </el-tab-pane> | ||||
|         </el-tabs> | ||||
|       </el-col> | ||||
|     </el-row> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { ProductVO } from '@/api/iot/product/product' | ||||
| import { SimulatorData, ThingModelApi } 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 { DataDefinition } from '@/views/iot/thingmodel/components' | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   product: ProductVO | ||||
|   device: DeviceVO | ||||
| }>() | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const activeTab = ref('up') // TODO @super:upstream 上行、downstream 下行 | ||||
| const subTab = ref('property') // TODO @super:upstreamTab | ||||
| 
 | ||||
| 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)) // 解析数据类型 | ||||
| 
 | ||||
| /** 查询物模型列表 */ | ||||
| // 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 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // // 功能列表数据结构定义 | ||||
| // 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 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 | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   try { | ||||
|     await DeviceApi.upstreamDevice({ | ||||
|       id: props.device.id, | ||||
|       type: 'property', | ||||
|       identifier: 'report', | ||||
|       data: data | ||||
|     }) | ||||
|     message.success('属性上报成功') | ||||
|   } 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 字符串 | ||||
| //   } | ||||
| 
 | ||||
| //   try { | ||||
| //     // TODO: 调用API发送数据 | ||||
| //     console.log('上报数据:', reportData) | ||||
| //     message.success('事件上报成功') | ||||
| //   } catch (error) { | ||||
| //     message.error('事件上报失败') | ||||
| //   } | ||||
| // } | ||||
| 
 | ||||
| /** 处理设备状态 */ | ||||
| const handleDeviceState = async (state: number) => { | ||||
|   try { | ||||
|     await DeviceApi.upstreamDevice({ | ||||
|       id: props.device.id, | ||||
|       type: 'state', | ||||
|       identifier: 'report', | ||||
|       data: state | ||||
|     }) | ||||
|     message.success(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}成功`) | ||||
|   } catch (error) { | ||||
|     message.error(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}失败`) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 处理属性获取 | ||||
| const handlePropertyGet = async () => { | ||||
|   // TODO: 实现属性获取逻辑 | ||||
|   message.success('属性获取成功') | ||||
| } | ||||
| 
 | ||||
| // 初始化 | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| // TODO @芋艿:后续再详细 review 下; | ||||
| </script> | ||||
|  | @ -0,0 +1,88 @@ | |||
| <template> | ||||
|   <DeviceDetailsHeader | ||||
|     :loading="loading" | ||||
|     :product="product" | ||||
|     :device="device" | ||||
|     @refresh="getDeviceData(id)" | ||||
|   /> | ||||
|   <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" /> | ||||
|       </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> | ||||
|       <el-tab-pane label="模拟设备" name="simulator"> | ||||
|         <DeviceDetailsSimulator | ||||
|           v-if="activeTab === 'simulator'" | ||||
|           :product="product" | ||||
|           :device="device" | ||||
|         /> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane label="设备配置" name="config"> | ||||
|         <DeviceDetailConfig | ||||
|           v-if="activeTab === 'config'" | ||||
|           :device="device" | ||||
|           @success="getDeviceData" | ||||
|         /> | ||||
|       </el-tab-pane> | ||||
|     </el-tabs> | ||||
|   </el-col> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
| import { DeviceApi, DeviceVO } from '@/api/iot/device/device' | ||||
| import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product' | ||||
| import DeviceDetailsHeader from './DeviceDetailsHeader.vue' | ||||
| import DeviceDetailsInfo from './DeviceDetailsInfo.vue' | ||||
| import DeviceDetailsModel from './DeviceDetailsModel.vue' | ||||
| import DeviceDetailsLog from './DeviceDetailsLog.vue' | ||||
| import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue' | ||||
| import DeviceDetailConfig from './DeviceDetailConfig.vue' | ||||
| 
 | ||||
| defineOptions({ name: 'IoTDeviceDetail' }) | ||||
| 
 | ||||
| const route = useRoute() | ||||
| const message = useMessage() | ||||
| const id = route.params.id // 将字符串转换为数字 | ||||
| const loading = ref(true) // 加载中 | ||||
| const product = ref<ProductVO>({} as ProductVO) // 产品详情 | ||||
| const device = ref<DeviceVO>({} as DeviceVO) // 设备详情 | ||||
| const activeTab = ref('info') // 默认激活的标签页 | ||||
| 
 | ||||
| /** 获取设备详情 */ | ||||
| const getDeviceData = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     device.value = await DeviceApi.getDevice(id) | ||||
|     await getProductData(device.value.productId) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 获取产品详情 */ | ||||
| const getProductData = async (id: number) => { | ||||
|   product.value = await ProductApi.getProduct(id) | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| const { delView } = useTagsViewStore() // 视图操作 | ||||
| const { currentRoute } = useRouter() // 路由 | ||||
| onMounted(async () => { | ||||
|   if (!id) { | ||||
|     message.warning('参数错误,产品不能为空!') | ||||
|     delView(unref(currentRoute)) | ||||
|     return | ||||
|   } | ||||
|   await getDeviceData() | ||||
|   activeTab.value = (route.query.tab as string) || 'info' | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,516 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <!-- 搜索工作栏 --> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item 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 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:device:create']" | ||||
|         > | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> | ||||
|           新增 | ||||
|         </el-button> | ||||
|         <el-button | ||||
|           type="success" | ||||
|           plain | ||||
|           @click="handleExport" | ||||
|           :loading="exportLoading" | ||||
|           v-hasPermi="['iot:device:export']" | ||||
|         > | ||||
|           <Icon icon="ep:download" class="mr-5px" /> 导出 | ||||
|         </el-button> | ||||
|         <el-button type="warning" plain @click="handleImport" v-hasPermi="['iot:device:import']"> | ||||
|           <Icon icon="ep:upload" /> 导入 | ||||
|         </el-button> | ||||
|         <el-button | ||||
|           type="primary" | ||||
|           plain | ||||
|           @click="openGroupForm" | ||||
|           :disabled="selectedIds.length === 0" | ||||
|           v-hasPermi="['iot:device:update']" | ||||
|         > | ||||
|           <Icon icon="ep:folder-add" class="mr-5px" /> 添加到分组 | ||||
|         </el-button> | ||||
|         <el-button | ||||
|           type="danger" | ||||
|           plain | ||||
|           @click="handleDeleteList" | ||||
|           :disabled="selectedIds.length === 0" | ||||
|           v-hasPermi="['iot:device:delete']" | ||||
|         > | ||||
|           <Icon icon="ep:delete" class="mr-5px" /> 批量删除 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <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="absolute top-0 left-0 right-0 h-[50px] pointer-events-none" | ||||
|               :class="[ | ||||
|                 item.state === DeviceStateEnum.ONLINE | ||||
|                   ? 'bg-gradient-to-b from-[#eefaff] to-transparent' | ||||
|                   : 'bg-gradient-to-b from-[#fff1f1] to-transparent' | ||||
|               ]" | ||||
|             > | ||||
|             </div> | ||||
|             <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.deviceName }}</div> | ||||
|                 <!-- 添加设备状态标签 --> | ||||
|                 <div class="inline-flex items-center"> | ||||
|                   <div | ||||
|                     class="w-1 h-1 rounded-full mr-1.5" | ||||
|                     :class=" | ||||
|                       item.state === DeviceStateEnum.ONLINE | ||||
|                         ? 'bg-[var(--el-color-success)]' | ||||
|                         : 'bg-[var(--el-color-danger)]' | ||||
|                     " | ||||
|                   > | ||||
|                   </div> | ||||
|                   <el-text | ||||
|                     class="!text-xs font-bold" | ||||
|                     :type="item.state === DeviceStateEnum.ONLINE ? 'success' : 'danger'" | ||||
|                   > | ||||
|                     {{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATE, item.state) }} | ||||
|                   </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-[#0070ff]"> | ||||
|                       {{ products.find((p) => p.id === item.productId)?.name }} | ||||
|                     </span> | ||||
|                   </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-[#0b1d30] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[130px]" | ||||
|                     > | ||||
|                       {{ item.deviceKey }} | ||||
|                     </span> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div class="w-[100px] h-[100px]"> | ||||
|                   <el-image :src="defaultPicUrl" class="w-full h-full" /> | ||||
|                 </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:device: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> | ||||
|                 <el-button | ||||
|                   class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]" | ||||
|                   type="info" | ||||
|                   plain | ||||
|                   @click="openModel(item.id)" | ||||
|                 > | ||||
|                   <Icon icon="ep:tickets" 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> | ||||
| 
 | ||||
|     <!-- 列表视图 --> | ||||
|     <el-table | ||||
|       v-else | ||||
|       v-loading="loading" | ||||
|       :data="list" | ||||
|       :stripe="true" | ||||
|       :show-overflow-tooltip="true" | ||||
|       @selection-change="handleSelectionChange" | ||||
|     > | ||||
|       <el-table-column type="selection" width="55" /> | ||||
|       <el-table-column label="DeviceName" align="center" prop="deviceName"> | ||||
|         <template #default="scope"> | ||||
|           <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <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-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="openModel(scope.row.id)"> 日志 </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="openForm('update', scope.row.id)" | ||||
|             v-hasPermi="['iot:device:update']" | ||||
|           > | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|             v-hasPermi="['iot:device:delete']" | ||||
|           > | ||||
|             删除 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
| 
 | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <DeviceForm ref="formRef" @success="getList" /> | ||||
|   <!-- 分组表单组件 --> | ||||
|   <DeviceGroupForm ref="groupFormRef" @success="getList" /> | ||||
|   <!-- 导入表单组件 --> | ||||
|   <DeviceImportForm ref="importFormRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { DeviceApi, DeviceVO, DeviceStateEnum } from '@/api/iot/device/device' | ||||
| import DeviceForm from './DeviceForm.vue' | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product/product' | ||||
| import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group' | ||||
| import download from '@/utils/download' | ||||
| import DeviceGroupForm from './DeviceGroupForm.vue' | ||||
| import DeviceImportForm from './DeviceImportForm.vue' | ||||
| 
 | ||||
| /** IoT 设备列表 */ | ||||
| defineOptions({ name: 'IoTDevice' }) | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表加载中 | ||||
| const list = ref<DeviceVO[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   deviceName: undefined, | ||||
|   productId: undefined, | ||||
|   deviceType: undefined, | ||||
|   nickname: undefined, | ||||
|   status: undefined, | ||||
|   groupId: undefined | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| const exportLoading = ref(false) // 导出加载状态 | ||||
| const products = ref<ProductVO[]>([]) // 产品列表 | ||||
| const deviceGroups = ref<DeviceGroupVO[]>([]) // 设备分组列表 | ||||
| const selectedIds = ref<number[]>([]) // 选中的设备编号数组 | ||||
| const viewMode = ref<'card' | 'list'>('card') // 视图模式状态 | ||||
| const defaultPicUrl = ref('/src/assets/imgs/iot/device.png') // 默认设备图片 | ||||
| const defaultIconUrl = ref('/src/assets/svgs/iot/card-fill.svg') // 默认设备图标 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     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() | ||||
|   selectedIds.value = [] // 清空选择 | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| const openForm = (type: string, id?: number) => { | ||||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 打开详情 */ | ||||
| const { push } = useRouter() | ||||
| const openDetail = (id: number) => { | ||||
|   push({ name: 'IoTDeviceDetail', params: { id } }) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (id: number) => { | ||||
|   try { | ||||
|     // 删除的二次确认 | ||||
|     await message.delConfirm() | ||||
|     // 起删除 | ||||
|     await DeviceApi.deleteDevice(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 导出方法 */ | ||||
| const handleExport = async () => { | ||||
|   try { | ||||
|     // 导出的二次确认 | ||||
|     await message.exportConfirm() | ||||
|     // 发起导出 | ||||
|     exportLoading.value = true | ||||
|     const data = await DeviceApi.exportDeviceExcel(queryParams) | ||||
|     download.excel(data, '物联网设备.xls') | ||||
|   } catch { | ||||
|   } finally { | ||||
|     exportLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 多选框选中数据 */ | ||||
| const handleSelectionChange = (selection: DeviceVO[]) => { | ||||
|   selectedIds.value = selection.map((item) => item.id) | ||||
| } | ||||
| 
 | ||||
| /** 批量删除按钮操作 */ | ||||
| const handleDeleteList = async () => { | ||||
|   try { | ||||
|     await message.delConfirm() | ||||
|     // 执行批量删除 | ||||
|     await DeviceApi.deleteDeviceList(selectedIds.value) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 添加到分组操作 */ | ||||
| const groupFormRef = ref() | ||||
| const openGroupForm = () => { | ||||
|   groupFormRef.value.open(selectedIds.value) | ||||
| } | ||||
| 
 | ||||
| /** 打开物模型数据 */ | ||||
| const openModel = (id: number) => { | ||||
|   push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } }) | ||||
| } | ||||
| 
 | ||||
| /** 设备导入 */ | ||||
| const importFormRef = ref() | ||||
| const handleImport = () => { | ||||
|   importFormRef.value.open() | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(async () => { | ||||
|   getList() | ||||
| 
 | ||||
|   // 获取产品列表 | ||||
|   products.value = await ProductApi.getSimpleProductList() | ||||
|   // 获取分组列表 | ||||
|   deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList() | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,112 @@ | |||
| <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="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="description"> | ||||
|         <el-input type="textarea" v-model="formData.description" placeholder="请输入分组描述" /> | ||||
|       </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 { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group' | ||||
| 
 | ||||
| /** IoT 设备分组 表单 */ | ||||
| defineOptions({ name: 'IoTDeviceGroupForm' }) | ||||
| 
 | ||||
| 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, | ||||
|   status: undefined, | ||||
|   description: undefined | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   name: [{ required: true, message: '分组名字不能为空', trigger: 'blur' }], | ||||
|   status: [{ 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 DeviceGroupApi.getDeviceGroup(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 DeviceGroupVO | ||||
|     if (formType.value === 'create') { | ||||
|       await DeviceGroupApi.createDeviceGroup(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await DeviceGroupApi.updateDeviceGroup(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     name: undefined, | ||||
|     status: undefined, | ||||
|     description: undefined | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -8,22 +8,24 @@ | |||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="产品名称" prop="name"> | ||||
|       <el-form-item label="分组名字" prop="name"> | ||||
|         <el-input | ||||
|           v-model="queryParams.name" | ||||
|           placeholder="请输入产品名称" | ||||
|           placeholder="请输入分组名字" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="ProductKey" prop="productKey"> | ||||
|         <el-input | ||||
|           v-model="queryParams.productKey" | ||||
|           placeholder="请输入产品标识" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|       <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> | ||||
|  | @ -33,7 +35,7 @@ | |||
|           type="primary" | ||||
|           plain | ||||
|           @click="openForm('create')" | ||||
|           v-hasPermi="['iot:product:create']" | ||||
|           v-hasPermi="['iot:device-group:create']" | ||||
|         > | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 新增 | ||||
|         </el-button> | ||||
|  | @ -44,17 +46,14 @@ | |||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <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="分组 ID" align="center" prop="id" /> | ||||
|       <el-table-column label="分组名字" align="center" prop="name" /> | ||||
|       <el-table-column label="分组状态" align="center" prop="status"> | ||||
|         <template #default="scope"> | ||||
|           <el-link @click="openDetail(scope.row.id)">{{ scope.row.name }}</el-link> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="ProductKey" align="center" prop="productKey" /> | ||||
|       <el-table-column label="设备类型" align="center" prop="deviceType"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" /> | ||||
|           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="分组描述" align="center" prop="description" /> | ||||
|       <el-table-column | ||||
|         label="创建时间" | ||||
|         align="center" | ||||
|  | @ -62,27 +61,22 @@ | |||
|         :formatter="dateFormatter" | ||||
|         width="180px" | ||||
|       /> | ||||
|       <el-table-column label="产品状态" align="center" prop="status"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="scope.row.status" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="操作" align="center"> | ||||
|       <el-table-column label="设备数量" align="center" prop="deviceCount" /> | ||||
|       <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']" | ||||
|             @click="openForm('update', scope.row.id)" | ||||
|             v-hasPermi="['iot:device-group:update']" | ||||
|           > | ||||
|             查看 | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|             v-hasPermi="['iot:product:delete']" | ||||
|             :disabled="scope.row.status === 1" | ||||
|             v-hasPermi="['iot:device-group:delete']" | ||||
|           > | ||||
|             删除 | ||||
|           </el-button> | ||||
|  | @ -99,39 +93,29 @@ | |||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <ProductForm ref="formRef" @success="getList" /> | ||||
|   <DeviceGroupForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product' | ||||
| import ProductForm from './ProductForm.vue' | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group' | ||||
| import DeviceGroupForm from './DeviceGroupForm.vue' | ||||
| 
 | ||||
| /** iot 产品 列表 */ | ||||
| defineOptions({ name: 'IoTProduct' }) | ||||
| /** IoT 设备分组列表 */ | ||||
| defineOptions({ name: 'IoTDeviceGroup' }) | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<ProductVO[]>([]) // 列表的数据 | ||||
| const list = ref<DeviceGroupVO[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: undefined, | ||||
|   createTime: [], | ||||
|   productKey: undefined, | ||||
|   protocolId: undefined, | ||||
|   categoryId: undefined, | ||||
|   description: undefined, | ||||
|   validateType: undefined, | ||||
|   status: undefined, | ||||
|   deviceType: undefined, | ||||
|   netType: undefined, | ||||
|   protocolType: undefined, | ||||
|   dataFormat: undefined | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
|  | @ -139,7 +123,7 @@ const queryFormRef = ref() // 搜索的表单 | |||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await ProductApi.getProductPage(queryParams) | ||||
|     const data = await DeviceGroupApi.getDeviceGroupPage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|  | @ -165,19 +149,13 @@ const openForm = (type: string, id?: number) => { | |||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 打开详情 */ | ||||
| const { push } = useRouter() | ||||
| const openDetail = (id: number) => { | ||||
|   push({ name: 'IoTProductDetail', params: { id } }) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (id: number) => { | ||||
|   try { | ||||
|     // 删除的二次确认 | ||||
|     await message.delConfirm() | ||||
|     // 发起删除 | ||||
|     await ProductApi.deleteProduct(id) | ||||
|     await DeviceGroupApi.deleteDeviceGroup(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|  | @ -1,267 +0,0 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <!-- 搜索工作栏 --> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item 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_STATUS)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </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:device:create']" | ||||
|         > | ||||
|           <Icon icon="ep:plus" 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="DeviceName" align="center" prop="deviceName"> | ||||
|         <template #default="scope"> | ||||
|           <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="备注名称" align="center" prop="nickname" /> | ||||
|       <el-table-column label="设备所属产品" align="center" prop="productId"> | ||||
|         <template #default="scope"> | ||||
|           {{ productMap[scope.row.productId] }} | ||||
|         </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="status"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.status" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         label="最后上线时间" | ||||
|         align="center" | ||||
|         prop="lastOnlineTime" | ||||
|         :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:device:update']" | ||||
|           > | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|             v-hasPermi="['iot:device:delete']" | ||||
|           > | ||||
|             删除 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <DeviceForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { DeviceApi, DeviceVO } from '@/api/iot/device' | ||||
| import DeviceForm from './DeviceForm.vue' | ||||
| import { ProductApi } from '@/api/iot/product' | ||||
| 
 | ||||
| /** IoT 设备 列表 */ | ||||
| defineOptions({ name: 'IoTDevice' }) | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<DeviceVO[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   deviceName: undefined, | ||||
|   productId: undefined, | ||||
|   deviceType: undefined, | ||||
|   nickname: undefined, | ||||
|   status: undefined | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
| /** 产品标号和名称的映射 */ | ||||
| const productMap = reactive({}) | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await DeviceApi.getDevicePage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|     // 获取产品ID列表 | ||||
|     const productIds = [...new Set(data.list.map((device) => device.productId))] | ||||
|     // 获取产品名称 | ||||
|     // TODO @haohao:最好后端拼接哈 | ||||
|     const products = await Promise.all(productIds.map((id) => ProductApi.getProduct(id))) | ||||
|     products.forEach((product) => { | ||||
|       productMap[product.id] = product.name | ||||
|     }) | ||||
|   } 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: 'IoTDeviceDetail', params: { id } }) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (id: number) => { | ||||
|   try { | ||||
|     // 删除的二次确认 | ||||
|     await message.delConfirm() | ||||
|     // 发起删除 | ||||
|     await DeviceApi.deleteDevice(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 查询字典下拉列表 */ | ||||
| const products = ref() | ||||
| const getProducts = async () => { | ||||
|   products.value = await ProductApi.getSimpleProductList() | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
|   getProducts() | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,509 @@ | |||
| <template> | ||||
|   <!-- 第一行:统计卡片行 --> | ||||
|   <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> | ||||
|     </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> | ||||
|     </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> | ||||
|     </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> | ||||
|     </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> | ||||
|     </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> | ||||
|     </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> | ||||
|     </el-col> | ||||
|   </el-row> | ||||
| 
 | ||||
|   <!-- TODO 第四行:地图 --> | ||||
| </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,拆一拆组件 | ||||
| 
 | ||||
| /** 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, | ||||
|   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 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)' } | ||||
|           ]) | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(() => { | ||||
|   getStats() | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,106 @@ | |||
| <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,0 +1,99 @@ | |||
| <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,0 +1,120 @@ | |||
| <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,0 +1,329 @@ | |||
| <!-- 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,0 +1,119 @@ | |||
| <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="sort"> | ||||
|         <el-input v-model="formData.sort" placeholder="请输入分类排序" /> | ||||
|       </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="description"> | ||||
|         <el-input type="textarea" v-model="formData.description" placeholder="请输入分类描述" /> | ||||
|       </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 { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category' | ||||
| import { CommonStatusEnum } from '@/utils/constants' | ||||
| 
 | ||||
| /** IoT 产品分类 表单 */ | ||||
| defineOptions({ name: 'ProductCategoryForm' }) | ||||
| 
 | ||||
| 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, | ||||
|   sort: 0, | ||||
|   status: CommonStatusEnum.ENABLE, | ||||
|   description: undefined | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   name: [{ required: true, message: '分类名字不能为空', trigger: 'blur' }], | ||||
|   status: [{ required: true, message: '分类状态不能为空', trigger: 'blur' }], | ||||
|   sort: [{ 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 ProductCategoryApi.getProductCategory(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 ProductCategoryVO | ||||
|     if (formType.value === 'create') { | ||||
|       await ProductCategoryApi.createProductCategory(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await ProductCategoryApi.updateProductCategory(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     name: undefined, | ||||
|     sort: 0, | ||||
|     status: CommonStatusEnum.ENABLE, | ||||
|     description: undefined | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,170 @@ | |||
| <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="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:product-category:create']" | ||||
|         > | ||||
|           <Icon icon="ep:plus" 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="ID" align="center" prop="id" /> | ||||
|       <el-table-column label="名字" align="center" prop="name" /> | ||||
|       <el-table-column label="排序" align="center" prop="sort" /> | ||||
|       <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="description" /> | ||||
|       <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:product-category:update']" | ||||
|           > | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|             v-hasPermi="['iot:product-category: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> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <ProductCategoryForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category' | ||||
| import ProductCategoryForm from './ProductCategoryForm.vue' | ||||
| 
 | ||||
| /** IoT 产品分类列表 */ | ||||
| defineOptions({ name: 'IotProductCategory' }) | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<ProductCategoryVO[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: undefined, | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| const exportLoading = ref(false) // 导出的加载中 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await ProductCategoryApi.getProductCategoryPage(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 ProductCategoryApi.deleteProductCategory(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| </script> | ||||
|  | @ -1,44 +0,0 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <el-collapse v-model="activeNames"> | ||||
|       <el-descriptions :column="3" title="产品信息"> | ||||
|         <el-descriptions-item label="产品名称">{{ product.name }}</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="创建时间"> | ||||
|           {{ 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" /> | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item label="产品状态"> | ||||
|           <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" /> | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item | ||||
|           label="联网方式" | ||||
|           v-if="product.deviceType === 0 || product.deviceType === 2" | ||||
|         > | ||||
|           <dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" /> | ||||
|         </el-descriptions-item> | ||||
|         <el-descriptions-item label="接入网关协议" v-if="product.deviceType === 1"> | ||||
|           <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> | ||||
|     </el-collapse> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { ProductVO } from '@/api/iot/product' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| 
 | ||||
| const { product } = defineProps<{ product: ProductVO }>() | ||||
| 
 | ||||
| // 展示的折叠面板 | ||||
| const activeNames = ref(['basicInfo']) | ||||
| </script> | ||||
|  | @ -1,229 +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="type"> | ||||
|         <el-radio-group v-model="formData.type"> | ||||
|           <el-radio-button value="1"> 属性 </el-radio-button> | ||||
|           <el-radio-button value="2"> 服务 </el-radio-button> | ||||
|           <el-radio-button value="3"> 事件 </el-radio-button> | ||||
|         </el-radio-group> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="功能名称" prop="name"> | ||||
|         <el-input v-model="formData.name" placeholder="请输入功能名称" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="标识符" prop="identifier"> | ||||
|         <el-input | ||||
|           v-model="formData.identifier" | ||||
|           placeholder="请输入标识符" | ||||
|           :disabled="formType === 'update'" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="数据类型" prop="type"> | ||||
|         <el-select | ||||
|           v-model="formData.property.dataType.type" | ||||
|           placeholder="请选择数据类型" | ||||
|           :disabled="formType === 'update'" | ||||
|         > | ||||
|           <el-option key="int" label="int32 (整数型)" value="int" /> | ||||
|           <el-option key="float" label="float (单精度浮点型)" value="float" /> | ||||
|           <el-option key="double" label="double (双精度浮点型)" value="double" /> | ||||
|           <!--          <el-option key="text" label="text (文本型)" value="text" />--> | ||||
|           <!--          <el-option key="date" label="date (日期型)" value="date" />--> | ||||
|           <!--          <el-option key="bool" label="bool (布尔型)" value="bool" />--> | ||||
|           <!--          <el-option key="enum" label="enum (枚举型)" value="enum" />--> | ||||
|           <!--          <el-option key="struct" label="struct (结构体)" value="struct" />--> | ||||
|           <!--          <el-option key="array" label="array (数组)" value="array" />--> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="取值范围" prop="max"> | ||||
|         <el-input v-model="formData.property.dataType.specs.min" placeholder="请输入最小值" /> | ||||
|         <span class="mx-2">~</span> | ||||
|         <el-input v-model="formData.property.dataType.specs.max" placeholder="请输入最大值" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="步长" prop="step"> | ||||
|         <el-input v-model="formData.property.dataType.specs.step" placeholder="请输入步长" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="单位" prop="unit"> | ||||
|         <el-input v-model="formData.property.dataType.specs.unit" placeholder="请输入单位" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="读写类型" prop="accessMode"> | ||||
|         <el-radio-group v-model="formData.property.accessMode"> | ||||
|           <el-radio label="rw">读写</el-radio> | ||||
|           <el-radio label="r">只读</el-radio> | ||||
|         </el-radio-group> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="属性描述" prop="property.description"> | ||||
|         <el-input | ||||
|           type="textarea" | ||||
|           v-model="formData.property.description" | ||||
|           placeholder="请输入属性描述" | ||||
|         /> | ||||
|       </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 { ProductVO } from '@/api/iot/product' | ||||
| import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction' | ||||
| 
 | ||||
| const props = defineProps<{ product: ProductVO }>() | ||||
| 
 | ||||
| defineOptions({ name: 'ThinkModelFunctionForm' }) | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const message = useMessage() | ||||
| 
 | ||||
| const dialogVisible = ref(false) | ||||
| const dialogTitle = ref('') | ||||
| const formLoading = ref(false) | ||||
| const formType = ref('') | ||||
| const formData = ref({ | ||||
|   id: undefined, | ||||
|   productId: undefined, | ||||
|   productKey: undefined, | ||||
|   identifier: undefined, | ||||
|   name: undefined, | ||||
|   description: undefined, | ||||
|   type: '1', | ||||
|   property: { | ||||
|     identifier: undefined, | ||||
|     name: undefined, | ||||
|     accessMode: 'rw', | ||||
|     required: true, | ||||
|     dataType: { | ||||
|       type: undefined, | ||||
|       specs: { | ||||
|         min: undefined, | ||||
|         max: undefined, | ||||
|         step: undefined, | ||||
|         unit: undefined | ||||
|       } | ||||
|     }, | ||||
|     description: undefined // 添加这一行 | ||||
|   } | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   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: (rule, value, callback) => { | ||||
|         const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value'] | ||||
|         if (reservedKeywords.includes(value)) { | ||||
|           callback( | ||||
|             new Error( | ||||
|               'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义' | ||||
|             ) | ||||
|           ) | ||||
|         } else { | ||||
|           callback() | ||||
|         } | ||||
|       }, | ||||
|       trigger: 'blur' | ||||
|     } | ||||
|   ], | ||||
|   property: { | ||||
|     dataType: { | ||||
|       type: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }] | ||||
|     }, | ||||
|     accessMode: [{ required: true, message: '读写类型不能为空', trigger: 'blur' }] | ||||
|   } | ||||
| }) | ||||
| const formRef = 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 ThinkModelFunctionApi.getThinkModelFunction(id) | ||||
|     } finally { | ||||
|       formLoading.value = false | ||||
|     } | ||||
|   } | ||||
| } | ||||
| defineExpose({ open, close: () => (dialogVisible.value = false) }) | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) | ||||
| const submitForm = async () => { | ||||
|   await formRef.value.validate() | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     const data = formData.value as unknown as ThinkModelFunctionVO | ||||
|     data.productId = props.product.id | ||||
|     data.productKey = props.product.productKey | ||||
|     if (formType.value === 'create') { | ||||
|       await ThinkModelFunctionApi.createThinkModelFunction(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await ThinkModelFunctionApi.updateThinkModelFunction(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false // 确保关闭弹框 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     productId: undefined, | ||||
|     productKey: undefined, | ||||
|     identifier: undefined, | ||||
|     name: undefined, | ||||
|     description: undefined, | ||||
|     type: '1', // todo @HAOHAO:看看枚举下 | ||||
|     property: { | ||||
|       identifier: undefined, | ||||
|       name: undefined, | ||||
|       accessMode: 'rw', | ||||
|       required: true, | ||||
|       dataType: { | ||||
|         type: undefined, | ||||
|         specs: { | ||||
|           min: undefined, | ||||
|           max: undefined, | ||||
|           step: undefined, | ||||
|           unit: undefined | ||||
|         } | ||||
|       }, | ||||
|       description: undefined // 确保重置 description 字段 | ||||
|     } | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -4,30 +4,48 @@ | |||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="formRules" | ||||
|       label-width="100px" | ||||
|       label-width="110px" | ||||
|       v-loading="formLoading" | ||||
|     > | ||||
|       <el-form-item label="ProductKey" prop="productKey"> | ||||
|         <el-input | ||||
|           v-model="formData.productKey" | ||||
|           placeholder="请输入 ProductKey" | ||||
|           :readonly="formType === 'update'" | ||||
|         > | ||||
|           <template #append> | ||||
|             <el-button @click="generateProductKey" :disabled="formType === 'update'"> | ||||
|               重新生成 | ||||
|             </el-button> | ||||
|           </template> | ||||
|         </el-input> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="产品名称" prop="name"> | ||||
|         <el-input v-model="formData.name" placeholder="请输入产品名称" /> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-form-item label="设备类型" prop="deviceType"> | ||||
|         <el-select | ||||
|           v-model="formData.deviceType" | ||||
|           placeholder="请选择设备类型" | ||||
|           :disabled="formType === 'update'" | ||||
|         > | ||||
|       <el-form-item label="产品分类" prop="categoryId"> | ||||
|         <el-select v-model="formData.categoryId" placeholder="请选择产品分类" clearable> | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|             v-for="category in categoryList" | ||||
|             :key="category.id" | ||||
|             :label="category.name" | ||||
|             :value="category.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-form-item label="设备类型" prop="deviceType"> | ||||
|         <el-radio-group v-model="formData.deviceType" :disabled="formType === 'update'"> | ||||
|           <el-radio | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.value" | ||||
|           > | ||||
|             {{ dict.label }} | ||||
|           </el-radio> | ||||
|         </el-radio-group> | ||||
|       </el-form-item> | ||||
|       <el-form-item | ||||
|         v-if="formData.deviceType === 0 || formData.deviceType === 2" | ||||
|         v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(formData.deviceType)" | ||||
|         label="联网方式" | ||||
|         prop="netType" | ||||
|       > | ||||
|  | @ -44,8 +62,11 @@ | |||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-form-item v-if="formData.deviceType === 1" label="接入网关协议" prop="protocolType"> | ||||
|       <el-form-item | ||||
|         v-if="formData.deviceType === DeviceTypeEnum.GATEWAY_SUB" | ||||
|         label="接入网关协议" | ||||
|         prop="protocolType" | ||||
|       > | ||||
|         <el-select | ||||
|           v-model="formData.protocolType" | ||||
|           placeholder="请选择接入网关协议" | ||||
|  | @ -59,22 +80,17 @@ | |||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-form-item label="数据格式" prop="dataFormat"> | ||||
|         <el-select | ||||
|           v-model="formData.dataFormat" | ||||
|           placeholder="请选择接数据格式" | ||||
|           :disabled="formType === 'update'" | ||||
|         > | ||||
|           <el-option | ||||
|         <el-radio-group v-model="formData.dataFormat" :disabled="formType === 'update'"> | ||||
|           <el-radio | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_FORMAT)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|             :label="dict.value" | ||||
|           > | ||||
|             {{ dict.label }} | ||||
|           </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-radio | ||||
|  | @ -86,12 +102,20 @@ | |||
|           </el-radio> | ||||
|         </el-radio-group> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-form-item label="产品描述" prop="description"> | ||||
|         <el-input type="textarea" v-model="formData.description" placeholder="请输入产品描述" /> | ||||
|       </el-form-item> | ||||
|       <el-collapse> | ||||
|         <el-collapse-item title="更多配置"> | ||||
|           <el-form-item label="产品图标" prop="icon"> | ||||
|             <UploadImg v-model="formData.icon" :height="'80px'" :width="'80px'" /> | ||||
|           </el-form-item> | ||||
|           <el-form-item label="产品图片" prop="picUrl"> | ||||
|             <UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" /> | ||||
|           </el-form-item> | ||||
|           <el-form-item label="产品描述" prop="description"> | ||||
|             <el-input type="textarea" v-model="formData.description" placeholder="请输入产品描述" /> | ||||
|           </el-form-item> | ||||
|         </el-collapse-item> | ||||
|       </el-collapse> | ||||
|     </el-form> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> | ||||
|       <el-button @click="dialogVisible = false">取 消</el-button> | ||||
|  | @ -100,8 +124,17 @@ | |||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product' | ||||
| import { | ||||
|   ValidateTypeEnum, | ||||
|   ProductApi, | ||||
|   ProductVO, | ||||
|   DataFormatEnum, | ||||
|   DeviceTypeEnum | ||||
| } from '@/api/iot/product/product' | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category' | ||||
| import { UploadImg } from '@/components/UploadFile' | ||||
| import { generateRandomStr } from '@/utils' | ||||
| 
 | ||||
| defineOptions({ name: 'IoTProductForm' }) | ||||
| 
 | ||||
|  | @ -113,37 +146,44 @@ const dialogTitle = ref('') | |||
| const formLoading = ref(false) | ||||
| const formType = ref('') | ||||
| const formData = ref({ | ||||
|   name: undefined, | ||||
|   id: undefined, | ||||
|   productKey: undefined, | ||||
|   protocolId: undefined, | ||||
|   name: undefined, | ||||
|   productKey: '', | ||||
|   categoryId: undefined, | ||||
|   icon: undefined, | ||||
|   picUrl: undefined, | ||||
|   description: undefined, | ||||
|   validateType: undefined, | ||||
|   status: undefined, | ||||
|   deviceType: undefined, | ||||
|   netType: undefined, | ||||
|   protocolType: undefined, | ||||
|   dataFormat: undefined | ||||
|   protocolId: undefined, | ||||
|   dataFormat: DataFormatEnum.JSON, | ||||
|   validateType: ValidateTypeEnum.WEAK | ||||
| }) | ||||
| 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' }], | ||||
|   netType: [ | ||||
|     { | ||||
|       // TODO @haohao:0、1、/2 最好前端也枚举下;另外,这里的 required 可以直接设置为 true。然后表单那些 v-if。只要不存在,它自动就不校验了哈 | ||||
|       required: formData.deviceType === 0 || formData.deviceType === 2, | ||||
|       required: true, | ||||
|       message: '联网方式不能为空', | ||||
|       trigger: 'change' | ||||
|     } | ||||
|   ], | ||||
|   protocolType: [ | ||||
|     { required: formData.deviceType === 1, message: '接入网关协议不能为空', trigger: 'change' } | ||||
|     { | ||||
|       required: true, | ||||
|       message: '接入网关协议不能为空', | ||||
|       trigger: 'change' | ||||
|     } | ||||
|   ], | ||||
|   dataFormat: [{ required: true, message: '数据格式不能为空', trigger: 'change' }], | ||||
|   validateType: [{ required: true, message: '数据校验级别不能为空', trigger: 'change' }] | ||||
| }) | ||||
| const formRef = ref() | ||||
| const categoryList = ref<ProductCategoryVO[]>([]) // 产品分类列表 | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async (type: string, id?: number) => { | ||||
|  | @ -158,7 +198,12 @@ const open = async (type: string, id?: number) => { | |||
|     } finally { | ||||
|       formLoading.value = false | ||||
|     } | ||||
|   } else { | ||||
|     // 新增时,生成随机 productKey | ||||
|     generateProductKey() | ||||
|   } | ||||
|   // 加载分类列表 | ||||
|   categoryList.value = await ProductCategoryApi.getSimpleProductCategoryList() | ||||
| } | ||||
| defineExpose({ open, close: () => (dialogVisible.value = false) }) | ||||
| 
 | ||||
|  | @ -186,19 +231,25 @@ const submitForm = async () => { | |||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     name: undefined, | ||||
|     id: undefined, | ||||
|     productKey: undefined, | ||||
|     protocolId: undefined, | ||||
|     name: undefined, | ||||
|     productKey: '', | ||||
|     categoryId: undefined, | ||||
|     icon: undefined, | ||||
|     picUrl: undefined, | ||||
|     description: undefined, | ||||
|     validateType: undefined, | ||||
|     status: undefined, | ||||
|     deviceType: undefined, | ||||
|     netType: undefined, | ||||
|     protocolType: undefined, | ||||
|     dataFormat: undefined | ||||
|     protocolId: undefined, | ||||
|     dataFormat: DataFormatEnum.JSON, | ||||
|     validateType: ValidateTypeEnum.WEAK | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| 
 | ||||
| /** 生成 ProductKey */ | ||||
| const generateProductKey = () => { | ||||
|   formData.value.productKey = generateRandomStr(16) | ||||
| } | ||||
| </script> | ||||
|  | @ -45,8 +45,8 @@ | |||
|     </el-descriptions> | ||||
|     <el-descriptions :column="5" direction="horizontal"> | ||||
|       <el-descriptions-item label="设备数"> | ||||
|         {{ product.deviceCount }} | ||||
|         <el-button @click="goToManagement(product.id)">前往管理</el-button> | ||||
|         {{ product.deviceCount ?? '加载中...' }} | ||||
|         <el-button @click="goToDeviceList(product.id)">前往管理</el-button> | ||||
|       </el-descriptions-item> | ||||
|     </el-descriptions> | ||||
|   </ContentWrap> | ||||
|  | @ -54,32 +54,37 @@ | |||
|   <ProductForm ref="formRef" @success="emit('refresh')" /> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import ProductForm from '@/views/iot/product/ProductForm.vue' | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product' | ||||
| import ProductForm from '@/views/iot/product/product/ProductForm.vue' | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product/product' | ||||
| 
 | ||||
| const message = useMessage() | ||||
| 
 | ||||
| const { product } = defineProps<{ product: ProductVO }>() // 定义 Props | ||||
| 
 | ||||
| /** 处理复制 */ | ||||
| const copyToClipboard = (text: string) => { | ||||
|   navigator.clipboard.writeText(text).then(() => { | ||||
| /** 复制到剪贴板方法 */ | ||||
| const copyToClipboard = async (text: string) => { | ||||
|   try { | ||||
|     await navigator.clipboard.writeText(text) | ||||
|     message.success('复制成功') | ||||
|   }) | ||||
|   } catch (error) { | ||||
|     message.error('复制失败') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 路由跳转到设备管理 */ | ||||
| const { push } = useRouter() | ||||
| const goToManagement = (productId: string) => { | ||||
|   push({ name: 'IoTDevice', query: { productId } }) | ||||
| const goToDeviceList = (productId: number) => { | ||||
|   push({ name: 'IoTDevice', params: { productId } }) | ||||
| } | ||||
| 
 | ||||
| /** 操作修改 */ | ||||
| /** 修改操作 */ | ||||
| const emit = defineEmits(['refresh']) // 定义 Emits | ||||
| const formRef = ref() | ||||
| const openForm = (type: string, id?: number) => { | ||||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 发布操作 */ | ||||
| const confirmPublish = async (id: number) => { | ||||
|   try { | ||||
|     await ProductApi.updateProductStatus(id, 1) | ||||
|  | @ -90,6 +95,8 @@ const confirmPublish = async (id: number) => { | |||
|     message.error('发布失败') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 撤销发布操作 */ | ||||
| const confirmUnpublish = async (id: number) => { | ||||
|   try { | ||||
|     await ProductApi.updateProductStatus(id, 0) | ||||
|  | @ -0,0 +1,43 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <el-descriptions :column="3" title="产品信息"> | ||||
|       <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="创建时间"> | ||||
|         {{ 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" /> | ||||
|       </el-descriptions-item> | ||||
|       <el-descriptions-item label="产品状态"> | ||||
|         <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" /> | ||||
|       </el-descriptions-item> | ||||
|       <el-descriptions-item | ||||
|         label="联网方式" | ||||
|         v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(product.deviceType)" | ||||
|       > | ||||
|         <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> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { DeviceTypeEnum, ProductVO } from '@/api/iot/product/product' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| 
 | ||||
| const { product } = defineProps<{ product: ProductVO }>() | ||||
| </script> | ||||
|  | @ -3,9 +3,9 @@ | |||
|     <el-tabs> | ||||
|       <el-tab-pane label="基础通信 Topic"> | ||||
|         <Table | ||||
|           :columns="columns1" | ||||
|           :data="data1" | ||||
|           :span-method="createSpanMethod(data1)" | ||||
|           :columns="basicColumn" | ||||
|           :data="basicData" | ||||
|           :span-method="createSpanMethod(basicData)" | ||||
|           align="left" | ||||
|           headerAlign="left" | ||||
|           border="true" | ||||
|  | @ -13,9 +13,9 @@ | |||
|       </el-tab-pane> | ||||
|       <el-tab-pane label="物模型通信 Topic"> | ||||
|         <Table | ||||
|           :columns="columns2" | ||||
|           :data="data2" | ||||
|           :span-method="createSpanMethod(data2)" | ||||
|           :columns="functionColumn" | ||||
|           :data="functionData" | ||||
|           :span-method="createSpanMethod(functionData)" | ||||
|           align="left" | ||||
|           headerAlign="left" | ||||
|           border="true" | ||||
|  | @ -25,27 +25,22 @@ | |||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { ProductVO } from '@/api/iot/product' | ||||
| import { ProductVO } from '@/api/iot/product/product' | ||||
| 
 | ||||
| const props = defineProps<{ product: ProductVO }>() | ||||
| 
 | ||||
| // 定义列 | ||||
| const columns1 = reactive([ | ||||
| // TODO 芋艿:不确定未来会不会改,所以先写死 | ||||
| 
 | ||||
| // 基础通信 Topic 列 | ||||
| const basicColumn = reactive([ | ||||
|   { label: '功能', field: 'function', width: 150 }, | ||||
|   { label: 'Topic 类', field: 'topicClass', width: 800 }, | ||||
|   { label: '操作权限', field: 'operationPermission', width: 100 }, | ||||
|   { label: '描述', field: 'description' } | ||||
| ]) | ||||
| 
 | ||||
| const columns2 = reactive([ | ||||
|   { label: '功能', field: 'function', width: 150 }, | ||||
|   { label: 'Topic 类', field: 'topicClass', width: 800 }, | ||||
|   { label: '操作权限', field: 'operationPermission', width: 100 }, | ||||
|   { label: '描述', field: 'description' } | ||||
| ]) | ||||
| 
 | ||||
| // TODO @haohao:这个,有没可能写到一个枚举里,方便后续维护? /Users/yunai/Java/yudao-ui-admin-vue3/src/views/ai/utils/constants.ts | ||||
| const data1 = computed(() => { | ||||
| // 基础通信 Topic 数据 | ||||
| const basicData = computed(() => { | ||||
|   if (!props.product || !props.product.productKey) return [] | ||||
|   return [ | ||||
|     { | ||||
|  | @ -147,7 +142,16 @@ const data1 = computed(() => { | |||
|   ] | ||||
| }) | ||||
| 
 | ||||
| const data2 = computed(() => { | ||||
| // 物模型通信 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 [ | ||||
|     { | ||||
|  | @ -8,8 +8,8 @@ | |||
|       <el-tab-pane label="Topic 类列表" name="topic"> | ||||
|         <ProductTopic v-if="activeTab === 'topic'" :product="product" /> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane label="功能定义" name="function"> | ||||
|         <ThinkModelFunction v-if="activeTab === 'function'" :product="product" /> | ||||
|       <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" /> | ||||
|  | @ -17,14 +17,15 @@ | |||
|   </el-col> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product' | ||||
| import { DeviceApi } from '@/api/iot/device' | ||||
| import ProductDetailsHeader from '@/views/iot/product/detail/ProductDetailsHeader.vue' | ||||
| import ProductDetailsInfo from '@/views/iot/product/detail/ProductDetailsInfo.vue' | ||||
| import ProductTopic from '@/views/iot/product/detail/ProductTopic.vue' | ||||
| import ThinkModelFunction from '@/views/iot/product/detail/ThinkModelFunction.vue' | ||||
| 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' | ||||
| import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants' | ||||
| 
 | ||||
| defineOptions({ name: 'IoTProductDetail' }) | ||||
| 
 | ||||
|  | @ -36,27 +37,26 @@ const message = useMessage() | |||
| const id = route.params.id // 编号 | ||||
| const loading = ref(true) // 加载中 | ||||
| const product = ref<ProductVO>({} as ProductVO) // 详情 | ||||
| const activeTab = ref('info') // 默认激活的标签页 | ||||
| const activeTab = ref('info') // 默认为 info 标签页 | ||||
| 
 | ||||
| provide(IOT_PROVIDE_KEY.PRODUCT, product) // 提供产品信息给产品信息详情页的所有子组件 | ||||
| 
 | ||||
| /** 获取详情 */ | ||||
| const getProductData = async (id: number) => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     product.value = await ProductApi.getProduct(id) | ||||
|     console.log('Product data:', product.value) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 查询设备数量 | ||||
| /** 查询设备数量 */ | ||||
| const getDeviceCount = async (productId: number) => { | ||||
|   try { | ||||
|     const count = await DeviceApi.getDeviceCount(productId) | ||||
|     console.log('Device count response:', count) | ||||
|     return count | ||||
|     return await DeviceApi.getDeviceCount(productId) | ||||
|   } catch (error) { | ||||
|     console.error('Error fetching device count:', error) | ||||
|     console.error('Error fetching device count:', error, 'productId:', productId) | ||||
|     return 0 | ||||
|   } | ||||
| } | ||||
|  | @ -69,12 +69,14 @@ onMounted(async () => { | |||
|     return | ||||
|   } | ||||
|   await getProductData(id) | ||||
|   // 处理 tab 参数 | ||||
|   const { tab } = route.query | ||||
|   if (tab) { | ||||
|     activeTab.value = tab as string | ||||
|   } | ||||
|   // 查询设备数量 | ||||
|   if (product.value.id) { | ||||
|     product.value.deviceCount = await getDeviceCount(product.value.id) | ||||
|     console.log('Device count:', product.value.deviceCount) | ||||
|   } else { | ||||
|     console.error('Product ID is undefined') | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,355 @@ | |||
| <template> | ||||
|   <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-button | ||||
|           v-hasPermi="['iot:product:create']" | ||||
|           plain | ||||
|           type="primary" | ||||
|           @click="openForm('create')" | ||||
|         > | ||||
|           <Icon class="mr-5px" icon="ep:plus" /> | ||||
|           新增 | ||||
|         </el-button> | ||||
|         <el-button | ||||
|           v-hasPermi="['iot:product:export']" | ||||
|           :loading="exportLoading" | ||||
|           plain | ||||
|           type="success" | ||||
|           @click="handleExport" | ||||
|         > | ||||
|           <Icon class="mr-5px" icon="ep:download" /> | ||||
|           导出 | ||||
|         </el-button> | ||||
|       </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> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 卡片视图 --> | ||||
|   <ContentWrap> | ||||
|     <el-row v-if="viewMode === 'card'" :gutter="16"> | ||||
|       <el-col v-for="item in list" :key="item.id" :lg="6" :md="12" :sm="12" :xs="24" class="mb-4"> | ||||
|         <el-card :body-style="{ padding: '0' }" class="h-full transition-colors"> | ||||
|           <!-- 内容区域 --> | ||||
|           <div class="p-4"> | ||||
|             <!-- 标题区域 --> | ||||
|             <div class="flex items-center mb-3"> | ||||
|               <div class="mr-2.5 flex items-center"> | ||||
|                 <el-image :src="item.icon || defaultIconUrl" class="w-[35px] h-[35px]" /> | ||||
|               </div> | ||||
|               <div class="text-[16px] font-600">{{ item.name }}</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-[#0070ff]">{{ item.categoryName }}</span> | ||||
|                 </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">产品标识</span> | ||||
|                   <span class="text-[#0b1d30] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]"> | ||||
|                     {{ item.productKey }} | ||||
|                   </span> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="w-[100px] h-[100px]"> | ||||
|                 <el-image :src="item.picUrl || defaultPicUrl" class="w-full h-full" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- 分隔线 --> | ||||
|             <el-divider class="!my-3" /> | ||||
| 
 | ||||
|             <!-- 按钮组 --> | ||||
|             <div class="flex items-center px-0"> | ||||
|               <el-button | ||||
|                 v-hasPermi="['iot:product:update']" | ||||
|                 class="flex-1 !px-2 !h-[32px] text-[13px]" | ||||
|                 plain | ||||
|                 type="primary" | ||||
|                 @click="openForm('update', item.id)" | ||||
|               > | ||||
|                 <Icon class="mr-1" icon="ep:edit-pen" /> | ||||
|                 编辑 | ||||
|               </el-button> | ||||
|               <el-button | ||||
|                 class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]" | ||||
|                 plain | ||||
|                 type="warning" | ||||
|                 @click="openDetail(item.id)" | ||||
|               > | ||||
|                 <Icon class="mr-1" icon="ep:view" /> | ||||
|                 详情 | ||||
|               </el-button> | ||||
|               <el-button | ||||
|                 class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]" | ||||
|                 plain | ||||
|                 type="success" | ||||
|                 @click="openObjectModel(item)" | ||||
|               > | ||||
|                 <Icon class="mr-1" icon="ep:scale-to-original" /> | ||||
|                 物模型 | ||||
|               </el-button> | ||||
|               <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div> | ||||
|               <el-button | ||||
|                 v-hasPermi="['iot:product:delete']" | ||||
|                 :disabled="item.status === 1" | ||||
|                 class="!px-2 !h-[32px] text-[13px]" | ||||
|                 plain | ||||
|                 type="danger" | ||||
|                 @click="handleDelete(item.id)" | ||||
|               > | ||||
|                 <Icon icon="ep:delete" /> | ||||
|               </el-button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|     </el-row> | ||||
| 
 | ||||
|     <!-- 列表视图 --> | ||||
|     <el-table v-else v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> | ||||
|       <el-table-column align="center" label="ID" prop="id" /> | ||||
|       <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-column align="center" label="操作"> | ||||
|         <template #default="scope"> | ||||
|           <el-button | ||||
|             v-hasPermi="['iot:product:query']" | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="openDetail(scope.row.id)" | ||||
|           > | ||||
|             查看 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             v-hasPermi="['iot:product:update']" | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="openForm('update', scope.row.id)" | ||||
|           > | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             v-hasPermi="['iot:product:delete']" | ||||
|             :disabled="scope.row.status === 1" | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|           > | ||||
|             删除 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
| 
 | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       :total="total" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <ProductForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { ProductApi, ProductVO } from '@/api/iot/product/product' | ||||
| import ProductForm from './ProductForm.vue' | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import download from '@/utils/download' | ||||
| import defaultPicUrl from '@/assets/imgs/iot/device.png' | ||||
| import defaultIconUrl from '@/assets/svgs/iot/cube.svg' | ||||
| 
 | ||||
| /** iot 产品列表 */ | ||||
| defineOptions({ name: 'IoTProduct' }) | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const { push } = useRouter() | ||||
| const route = useRoute() | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const activeName = ref('info') // 当前激活的标签页 | ||||
| const list = ref<ProductVO[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: undefined, | ||||
|   productKey: undefined | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| const exportLoading = ref(false) // 导出加载中 | ||||
| const viewMode = ref<'card' | 'list'>('card') // 视图模式状态 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| 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 formRef = ref() | ||||
| const openForm = (type: string, id?: number) => { | ||||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 打开详情 */ | ||||
| const openDetail = (id: number) => { | ||||
|   push({ name: 'IoTProductDetail', params: { id } }) | ||||
| } | ||||
| 
 | ||||
| /** 打开物模型 */ | ||||
| const openObjectModel = (item: ProductVO) => { | ||||
|   push({ | ||||
|     name: 'IoTProductDetail', | ||||
|     params: { id: item.id }, | ||||
|     query: { tab: 'thingModel' } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (id: number) => { | ||||
|   try { | ||||
|     // 删除的二次确认 | ||||
|     await message.delConfirm() | ||||
|     // 发起删除 | ||||
|     await ProductApi.deleteProduct(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 导出按钮操作 */ | ||||
| const handleExport = async () => { | ||||
|   try { | ||||
|     // 导出的二次确认 | ||||
|     await message.exportConfirm() | ||||
|     // 发起导出 | ||||
|     exportLoading.value = true | ||||
|     const data = await ProductApi.exportProduct(queryParams) | ||||
|     download.excel(data, '物联网产品.xls') | ||||
|   } catch { | ||||
|   } finally { | ||||
|     exportLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
|   // 处理 tab 参数 | ||||
|   const { tab } = route.query | ||||
|   if (tab) { | ||||
|     activeName.value = tab as string | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,207 @@ | |||
| <template> | ||||
|   <Dialog v-model="dialogVisible" :title="dialogTitle"> | ||||
|     <el-form | ||||
|       ref="formRef" | ||||
|       v-loading="formLoading" | ||||
|       :model="formData" | ||||
|       :rules="formRules" | ||||
|       label-width="120px" | ||||
|     > | ||||
|       <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> | ||||
|       <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> | ||||
|       <HttpConfigForm v-if="showConfig(IoTDataBridgeConfigType.HTTP)" v-model="formData.config" /> | ||||
|       <MqttConfigForm v-if="showConfig(IoTDataBridgeConfigType.MQTT)" v-model="formData.config" /> | ||||
|       <RocketMQConfigForm | ||||
|         v-if="showConfig(IoTDataBridgeConfigType.ROCKETMQ)" | ||||
|         v-model="formData.config" | ||||
|       /> | ||||
|       <KafkaMQConfigForm | ||||
|         v-if="showConfig(IoTDataBridgeConfigType.KAFKA)" | ||||
|         v-model="formData.config" | ||||
|       /> | ||||
|       <RabbitMQConfigForm | ||||
|         v-if="showConfig(IoTDataBridgeConfigType.RABBITMQ)" | ||||
|         v-model="formData.config" | ||||
|       /> | ||||
|       <RedisStreamMQConfigForm | ||||
|         v-if="showConfig(IoTDataBridgeConfigType.REDIS_STREAM)" | ||||
|         v-model="formData.config" | ||||
|       /> | ||||
|       <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="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> | ||||
|       <el-button @click="dialogVisible = false">取 消</el-button> | ||||
|     </template> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { DICT_TYPE, getDictObj, getIntDictOptions } from '@/utils/dict' | ||||
| import { DataBridgeApi, DataBridgeVO, IoTDataBridgeConfigType } from '@/api/iot/rule/databridge' | ||||
| import { | ||||
|   HttpConfigForm, | ||||
|   KafkaMQConfigForm, | ||||
|   MqttConfigForm, | ||||
|   RabbitMQConfigForm, | ||||
|   RedisStreamMQConfigForm, | ||||
|   RocketMQConfigForm | ||||
| } from './config' | ||||
| 
 | ||||
| /** IoT 数据桥梁的表单 */ | ||||
| defineOptions({ name: 'IoTDataBridgeForm' }) | ||||
| 
 | ||||
| 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<DataBridgeVO>({ | ||||
|   status: 0, | ||||
|   direction: 1, // TODO @puhui999:枚举类 | ||||
|   type: 1, // TODO @puhui999:枚举类 | ||||
|   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' }], | ||||
|   // 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.topic': [{ required: true, message: '主题不能为空', trigger: 'blur' }], | ||||
|   // RocketMQ 配置 | ||||
|   'config.nameServer': [{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }], | ||||
|   'config.accessKey': [{ required: true, message: 'AccessKey 不能为空', trigger: 'blur' }], | ||||
|   'config.secretKey': [{ required: true, message: 'SecretKey 不能为空', trigger: 'blur' }], | ||||
|   'config.group': [{ required: true, message: '消费组不能为空', trigger: 'blur' }], | ||||
|   // Kafka 配置 | ||||
|   'config.bootstrapServers': [{ required: true, message: '服务地址不能为空', trigger: 'blur' }], | ||||
|   'config.ssl': [{ required: true, message: 'SSL 配置不能为空', trigger: 'change' }], | ||||
|   // RabbitMQ 配置 | ||||
|   'config.host': [{ required: true, message: '主机地址不能为空', trigger: 'blur' }], | ||||
|   'config.port': [ | ||||
|     { required: true, message: '端口不能为空', trigger: 'blur' }, | ||||
|     { type: 'number', min: 1, max: 65535, message: '端口号范围 1-65535', trigger: 'blur' } | ||||
|   ], | ||||
|   'config.virtualHost': [{ required: true, message: '虚拟主机不能为空', trigger: 'blur' }], | ||||
|   'config.exchange': [{ required: true, message: '交换机不能为空', trigger: 'blur' }], | ||||
|   'config.routingKey': [{ required: true, message: '路由键不能为空', trigger: 'blur' }], | ||||
|   'config.queue': [{ required: true, message: '队列不能为空', trigger: 'blur' }], | ||||
|   // Redis Stream 配置 | ||||
|   'config.database': [ | ||||
|     { required: true, message: '数据库索引不能为空', trigger: 'blur' }, | ||||
|     { type: 'number', min: 0, message: '数据库索引必须是非负整数', trigger: 'blur' } | ||||
|   ] | ||||
| }) | ||||
| 
 | ||||
| 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) => { | ||||
|   dialogVisible.value = true | ||||
|   dialogTitle.value = t('action.' + type) | ||||
|   formType.value = type | ||||
|   resetForm() | ||||
|   // 修改时,设置数据 | ||||
|   if (id) { | ||||
|     formLoading.value = true | ||||
|     try { | ||||
|       formData.value = await DataBridgeApi.getDataBridge(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 DataBridgeVO | ||||
|     if (formType.value === 'create') { | ||||
|       await DataBridgeApi.createDataBridge(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await DataBridgeApi.updateDataBridge(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 处理类型切换事件 */ | ||||
| const handleTypeChange = (val: number) => { | ||||
|   formData.value.type = val | ||||
|   // 切换类型时重置配置 | ||||
|   formData.value.config = {} as any | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     // TODO @puhui999:换成枚举值哈 | ||||
|     status: 0, | ||||
|     direction: 1, | ||||
|     type: 1, | ||||
|     config: {} as any | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,84 @@ | |||
| <template> | ||||
|   <el-form-item label="请求地址" prop="config.url"> | ||||
|     <el-input v-model="urlPath" placeholder="请输入请求地址"> | ||||
|       <template #prepend> | ||||
|         <el-select v-model="urlPrefix" placeholder="Select" style="width: 115px"> | ||||
|           <el-option label="http://" value="http://" /> | ||||
|           <el-option label="https://" value="https://" /> | ||||
|         </el-select> | ||||
|       </template> | ||||
|     </el-input> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="请求方法" prop="config.method"> | ||||
|     <el-select v-model="config.method" placeholder="请选择请求方法"> | ||||
|       <el-option label="GET" value="GET" /> | ||||
|       <el-option label="POST" value="POST" /> | ||||
|       <el-option label="PUT" value="PUT" /> | ||||
|       <el-option label="DELETE" value="DELETE" /> | ||||
|     </el-select> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="请求头" prop="config.headers"> | ||||
|     <key-value-editor v-model="config.headers" add-button-text="添加请求头" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="请求参数" prop="config.query"> | ||||
|     <key-value-editor v-model="config.query" add-button-text="添加参数" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="请求体" prop="config.body"> | ||||
|     <el-input v-model="config.body" placeholder="请输入内容" type="textarea" /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { HttpConfig, IoTDataBridgeConfigType } from '@/api/iot/rule/databridge' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| import KeyValueEditor from './components/KeyValueEditor.vue' | ||||
| 
 | ||||
| defineOptions({ name: 'HttpConfigForm' }) | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: any | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const config = useVModel(props, 'modelValue', emit) as Ref<HttpConfig> | ||||
| 
 | ||||
| /** URL处理 */ | ||||
| const urlPrefix = ref('http://') | ||||
| const urlPath = ref('') | ||||
| const fullUrl = computed(() => { | ||||
|   return urlPath.value ? urlPrefix.value + urlPath.value : '' | ||||
| }) | ||||
| 
 | ||||
| /** 监听 URL 变化 */ | ||||
| watch([urlPrefix, urlPath], () => { | ||||
|   config.value.url = fullUrl.value | ||||
| }) | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(() => { | ||||
|   if (!isEmpty(config.value)) { | ||||
|     // 初始化 URL | ||||
|     if (config.value.url) { | ||||
|       if (config.value.url.startsWith('https://')) { | ||||
|         urlPrefix.value = 'https://' | ||||
|         urlPath.value = config.value.url.substring(8) | ||||
|       } else if (config.value.url.startsWith('http://')) { | ||||
|         urlPrefix.value = 'http://' | ||||
|         urlPath.value = config.value.url.substring(7) | ||||
|       } else { | ||||
|         urlPath.value = config.value.url | ||||
|       } | ||||
|     } | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   config.value = { | ||||
|     type: IoTDataBridgeConfigType.HTTP, | ||||
|     url: '', | ||||
|     method: 'POST', | ||||
|     headers: {}, | ||||
|     query: {}, | ||||
|     body: '' | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,45 @@ | |||
| <template> | ||||
|   <el-form-item label="服务地址" prop="config.bootstrapServers"> | ||||
|     <el-input v-model="config.bootstrapServers" placeholder="请输入服务地址,如:localhost:9092" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="用户名" prop="config.username"> | ||||
|     <el-input v-model="config.username" placeholder="请输入用户名" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="密码" prop="config.password"> | ||||
|     <el-input v-model="config.password" placeholder="请输入密码" show-password type="password" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="启用 SSL" prop="config.ssl"> | ||||
|     <el-switch v-model="config.ssl" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="主题" prop="config.topic"> | ||||
|     <el-input v-model="config.topic" placeholder="请输入主题" /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { IoTDataBridgeConfigType, KafkaMQConfig } from '@/api/iot/rule/databridge' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| defineOptions({ name: 'KafkaMQConfigForm' }) | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: any | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const config = useVModel(props, 'modelValue', emit) as Ref<KafkaMQConfig> | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(() => { | ||||
|   if (!isEmpty(config.value)) { | ||||
|     return | ||||
|   } | ||||
|   config.value = { | ||||
|     type: IoTDataBridgeConfigType.KAFKA, | ||||
|     bootstrapServers: '', | ||||
|     username: '', | ||||
|     password: '', | ||||
|     ssl: false, | ||||
|     topic: '' | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,45 @@ | |||
| <template> | ||||
|   <el-form-item label="服务地址" prop="config.url"> | ||||
|     <el-input v-model="config.url" placeholder="请输入MQTT服务地址,如:mqtt://localhost:1883" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="用户名" prop="config.username"> | ||||
|     <el-input v-model="config.username" placeholder="请输入用户名" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="密码" prop="config.password"> | ||||
|     <el-input v-model="config.password" placeholder="请输入密码" show-password type="password" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="客户端ID" prop="config.clientId"> | ||||
|     <el-input v-model="config.clientId" placeholder="请输入客户端ID" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="主题" prop="config.topic"> | ||||
|     <el-input v-model="config.topic" placeholder="请输入主题" /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { IoTDataBridgeConfigType, MqttConfig } from '@/api/iot/rule/databridge' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| defineOptions({ name: 'MqttConfigForm' }) | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: any | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const config = useVModel(props, 'modelValue', emit) as Ref<MqttConfig> | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(() => { | ||||
|   if (!isEmpty(config.value)) { | ||||
|     return | ||||
|   } | ||||
|   config.value = { | ||||
|     type: IoTDataBridgeConfigType.MQTT, | ||||
|     url: '', | ||||
|     username: '', | ||||
|     password: '', | ||||
|     clientId: '', | ||||
|     topic: '' | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,63 @@ | |||
| <template> | ||||
|   <el-form-item label="主机地址" prop="config.host"> | ||||
|     <el-input v-model="config.host" placeholder="请输入主机地址,如:localhost" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="端口" prop="config.port"> | ||||
|     <el-input-number | ||||
|       v-model="config.port" | ||||
|       :max="65535" | ||||
|       :min="1" | ||||
|       controls-position="right" | ||||
|       placeholder="请输入端口" | ||||
|     /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="虚拟主机" prop="config.virtualHost"> | ||||
|     <el-input v-model="config.virtualHost" placeholder="请输入虚拟主机" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="用户名" prop="config.username"> | ||||
|     <el-input v-model="config.username" placeholder="请输入用户名" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="密码" prop="config.password"> | ||||
|     <el-input v-model="config.password" placeholder="请输入密码" show-password type="password" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="交换机" prop="config.exchange"> | ||||
|     <el-input v-model="config.exchange" placeholder="请输入交换机" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="路由键" prop="config.routingKey"> | ||||
|     <el-input v-model="config.routingKey" placeholder="请输入路由键" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="队列" prop="config.queue"> | ||||
|     <el-input v-model="config.queue" placeholder="请输入队列" /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { IoTDataBridgeConfigType, RabbitMQConfig } from '@/api/iot/rule/databridge' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| defineOptions({ name: 'RabbitMQConfigForm' }) | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: any | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const config = useVModel(props, 'modelValue', emit) as Ref<RabbitMQConfig> | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(() => { | ||||
|   if (!isEmpty(config.value)) { | ||||
|     return | ||||
|   } | ||||
|   config.value = { | ||||
|     type: IoTDataBridgeConfigType.RABBITMQ, | ||||
|     host: '', | ||||
|     port: 5672, | ||||
|     virtualHost: '/', | ||||
|     username: '', | ||||
|     password: '', | ||||
|     exchange: '', | ||||
|     routingKey: '', | ||||
|     queue: '' | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,58 @@ | |||
| <!-- TODO @puhui999:去掉 MQ 关键字哈 --> | ||||
| <template> | ||||
|   <el-form-item label="主机地址" prop="config.host"> | ||||
|     <el-input v-model="config.host" placeholder="请输入主机地址,如:localhost" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="端口" prop="config.port"> | ||||
|     <el-input-number | ||||
|       v-model="config.port" | ||||
|       :max="65535" | ||||
|       :min="1" | ||||
|       controls-position="right" | ||||
|       placeholder="请输入端口" | ||||
|     /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="密码" prop="config.password"> | ||||
|     <el-input v-model="config.password" placeholder="请输入密码" show-password type="password" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="数据库" prop="config.database"> | ||||
|     <el-input-number | ||||
|       v-model="config.database" | ||||
|       :max="15" | ||||
|       :min="0" | ||||
|       controls-position="right" | ||||
|       placeholder="请输入数据库索引" | ||||
|     /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="主题" prop="config.topic"> | ||||
|     <el-input v-model="config.topic" placeholder="请输入主题" /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { IoTDataBridgeConfigType, RedisStreamMQConfig } from '@/api/iot/rule/databridge' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| defineOptions({ name: 'RedisStreamMQConfigForm' }) | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: any | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const config = useVModel(props, 'modelValue', emit) as Ref<RedisStreamMQConfig> | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(() => { | ||||
|   if (!isEmpty(config.value)) { | ||||
|     return | ||||
|   } | ||||
|   config.value = { | ||||
|     type: IoTDataBridgeConfigType.REDIS_STREAM, | ||||
|     host: '', | ||||
|     port: 6379, | ||||
|     password: '', | ||||
|     database: 0, | ||||
|     topic: '' | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,57 @@ | |||
| <template> | ||||
|   <el-form-item label="NameServer" prop="config.nameServer"> | ||||
|     <el-input | ||||
|       v-model="config.nameServer" | ||||
|       placeholder="请输入 NameServer 地址,如:127.0.0.1:9876" | ||||
|     /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="AccessKey" prop="config.accessKey"> | ||||
|     <el-input v-model="config.accessKey" placeholder="请输入 AccessKey" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="SecretKey" prop="config.secretKey"> | ||||
|     <el-input | ||||
|       v-model="config.secretKey" | ||||
|       placeholder="请输入 SecretKey" | ||||
|       show-password | ||||
|       type="password" | ||||
|     /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="消费组" prop="config.group"> | ||||
|     <el-input v-model="config.group" placeholder="请输入消费组" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="主题" prop="config.topic"> | ||||
|     <el-input v-model="config.topic" placeholder="请输入主题" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="标签" prop="config.tags"> | ||||
|     <el-input v-model="config.tags" placeholder="请输入标签" /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { IoTDataBridgeConfigType, RocketMQConfig } from '@/api/iot/rule/databridge' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| defineOptions({ name: 'RocketMQConfigForm' }) | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: any | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const config = useVModel(props, 'modelValue', emit) as Ref<RocketMQConfig> | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(() => { | ||||
|   if (!isEmpty(config.value)) { | ||||
|     return | ||||
|   } | ||||
|   config.value = { | ||||
|     type: IoTDataBridgeConfigType.ROCKETMQ, | ||||
|     nameServer: '', | ||||
|     accessKey: '', | ||||
|     secretKey: '', | ||||
|     group: '', | ||||
|     topic: '', | ||||
|     tags: '' | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,74 @@ | |||
| <template> | ||||
|   <div v-for="(item, index) in items" :key="index" class="flex mb-2 w-full"> | ||||
|     <el-input v-model="item.key" class="mr-2" placeholder="键" /> | ||||
|     <el-input v-model="item.value" placeholder="值" /> | ||||
|     <el-button class="ml-2" text type="danger" @click="removeItem(index)"> | ||||
|       <el-icon> | ||||
|         <Delete /> | ||||
|       </el-icon> | ||||
|       删除 | ||||
|     </el-button> | ||||
|   </div> | ||||
|   <el-button text type="primary" @click="addItem"> | ||||
|     <el-icon> | ||||
|       <Plus /> | ||||
|     </el-icon> | ||||
|     {{ addButtonText }} | ||||
|   </el-button> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { Delete, Plus } from '@element-plus/icons-vue' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| defineOptions({ name: 'KeyValueEditor' }) | ||||
| 
 | ||||
| interface KeyValueItem { | ||||
|   key: string | ||||
|   value: string | ||||
| } | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   modelValue: Record<string, string> | ||||
|   addButtonText: string | ||||
| }>() | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const items = ref<KeyValueItem[]>([]) // 内部 key-value 项列表 | ||||
| 
 | ||||
| /** 添加项目 */ | ||||
| const addItem = () => { | ||||
|   items.value.push({ key: '', value: '' }) | ||||
|   updateModelValue() | ||||
| } | ||||
| 
 | ||||
| /** 移除项目 */ | ||||
| const removeItem = (index: number) => { | ||||
|   items.value.splice(index, 1) | ||||
|   updateModelValue() | ||||
| } | ||||
| 
 | ||||
| /** 更新 modelValue */ | ||||
| const updateModelValue = () => { | ||||
|   const result: Record<string, string> = {} | ||||
|   items.value.forEach((item) => { | ||||
|     if (item.key) { | ||||
|       result[item.key] = item.value | ||||
|     } | ||||
|   }) | ||||
|   emit('update:modelValue', result) | ||||
| } | ||||
| 
 | ||||
| // TODO @puhui999:有告警的地方,尽量用 cursor 处理下 | ||||
| /** 监听项目变化 */ | ||||
| watch(items, updateModelValue, { deep: true }) | ||||
| watch( | ||||
|   () => props.modelValue, | ||||
|   (val) => { | ||||
|     // 列表有值后以列表中的值为准 | ||||
|     if (isEmpty(val) || !isEmpty(items.value)) { | ||||
|       return | ||||
|     } | ||||
|     items.value = Object.entries(props.modelValue).map(([key, value]) => ({ key, value })) | ||||
|   } | ||||
| ) | ||||
| </script> | ||||
|  | @ -0,0 +1,15 @@ | |||
| import HttpConfigForm from './HttpConfigForm.vue' | ||||
| 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' | ||||
| 
 | ||||
| export { | ||||
|   HttpConfigForm, | ||||
|   MqttConfigForm, | ||||
|   RocketMQConfigForm, | ||||
|   KafkaMQConfigForm, | ||||
|   RabbitMQConfigForm, | ||||
|   RedisStreamMQConfigForm | ||||
| } | ||||
|  | @ -0,0 +1,234 @@ | |||
| <template> | ||||
|   <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="桥梁状态" prop="status"> | ||||
|         <el-select | ||||
|           v-model="queryParams.status" | ||||
|           class="!w-240px" | ||||
|           clearable | ||||
|           placeholder="请选择桥梁状态" | ||||
|         > | ||||
|           <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="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-select | ||||
|           v-model="queryParams.type" | ||||
|           class="!w-240px" | ||||
|           clearable | ||||
|           placeholder="请选择桥梁类型" | ||||
|         > | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM)" | ||||
|             :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" | ||||
|           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" | ||||
|           class="!w-220px" | ||||
|           end-placeholder="结束日期" | ||||
|           start-placeholder="开始日期" | ||||
|           type="daterange" | ||||
|           value-format="YYYY-MM-DD HH:mm:ss" | ||||
|         /> | ||||
|       </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-button | ||||
|           v-hasPermi="['iot:data-bridge:create']" | ||||
|           plain | ||||
|           type="primary" | ||||
|           @click="openForm('create')" | ||||
|         > | ||||
|           <Icon class="mr-5px" icon="ep:plus" /> | ||||
|           新增 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <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"> | ||||
|         <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"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM" :value="scope.row.type" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         :formatter="dateFormatter" | ||||
|         align="center" | ||||
|         label="创建时间" | ||||
|         prop="createTime" | ||||
|         width="180px" | ||||
|       /> | ||||
|       <el-table-column align="center" fixed="right" label="操作" width="120px"> | ||||
|         <template #default="scope"> | ||||
|           <el-button | ||||
|             v-hasPermi="['iot:data-bridge:update']" | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="openForm('update', scope.row.id)" | ||||
|           > | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             v-hasPermi="['iot:data-bridge:delete']" | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|           > | ||||
|             删除 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       :total="total" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <DataBridgeForm 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' | ||||
| 
 | ||||
| /** IoT 数据桥梁 列表 */ | ||||
| defineOptions({ name: 'IotDataBridge' }) | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<DataBridgeVO[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: undefined, | ||||
|   description: undefined, | ||||
|   status: undefined, | ||||
|   direction: undefined, | ||||
|   type: undefined, | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await DataBridgeApi.getDataBridgePage(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 DataBridgeApi.deleteDataBridge(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| </script> | ||||
|  | @ -0,0 +1,56 @@ | |||
| <!-- 产品的物模型表单(event 项) --> | ||||
| <template> | ||||
|   <el-form-item | ||||
|     :rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]" | ||||
|     label="事件类型" | ||||
|     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> | ||||
|     </el-radio-group> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="输出参数"> | ||||
|     <ThingModelInputOutputParam | ||||
|       v-model="thingModelEvent.outputParams" | ||||
|       :direction="ThingModelParamDirection.OUTPUT" | ||||
|     /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| 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' | ||||
| 
 | ||||
| /** IoT 物模型事件 */ | ||||
| defineOptions({ name: 'ThingModelEvent' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<ThingModelEvent> | ||||
| 
 | ||||
| // 默认选中,INFO 信息 | ||||
| watch( | ||||
|   () => thingModelEvent.value.type, | ||||
|   (val: string) => isEmpty(val) && (thingModelEvent.value.type = ThingModelEventType.INFO.value), | ||||
|   { immediate: true } | ||||
| ) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| :deep(.el-form-item) { | ||||
|   .el-form-item { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,215 @@ | |||
| <!-- 产品的物模型表单 --> | ||||
| <template> | ||||
|   <Dialog v-model="dialogVisible" :title="dialogTitle"> | ||||
|     <el-form | ||||
|       ref="formRef" | ||||
|       v-loading="formLoading" | ||||
|       :model="formData" | ||||
|       :rules="ThingModelFormRules" | ||||
|       label-width="100px" | ||||
|     > | ||||
|       <el-form-item label="功能类型" prop="type"> | ||||
|         <el-radio-group v-model="formData.type"> | ||||
|           <el-radio-button | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)" | ||||
|             :key="dict.value" | ||||
|             :value="dict.value" | ||||
|           > | ||||
|             {{ dict.label }} | ||||
|           </el-radio-button> | ||||
|         </el-radio-group> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="功能名称" prop="name"> | ||||
|         <el-input v-model="formData.name" placeholder="请输入功能名称" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="标识符" prop="identifier"> | ||||
|         <el-input v-model="formData.identifier" placeholder="请输入标识符" /> | ||||
|       </el-form-item> | ||||
|       <!-- 属性配置 --> | ||||
|       <ThingModelProperty | ||||
|         v-if="formData.type === ThingModelType.PROPERTY" | ||||
|         v-model="formData.property" | ||||
|       /> | ||||
|       <!-- 服务配置 --> | ||||
|       <ThingModelService | ||||
|         v-if="formData.type === ThingModelType.SERVICE" | ||||
|         v-model="formData.service" | ||||
|       /> | ||||
|       <!-- 事件配置 --> | ||||
|       <ThingModelEvent v-if="formData.type === ThingModelType.EVENT" v-model="formData.event" /> | ||||
|       <el-form-item label="描述" prop="description"> | ||||
|         <el-input | ||||
|           v-model="formData.description" | ||||
|           :maxlength="200" | ||||
|           :rows="3" | ||||
|           placeholder="请输入属性描述" | ||||
|           type="textarea" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
| 
 | ||||
|     <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 { 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 { cloneDeep } from 'lodash-es' | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** IoT 物模型数据表单 */ | ||||
| defineOptions({ name: 'IoTThingModelForm' }) | ||||
| 
 | ||||
| const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息 | ||||
| 
 | ||||
| 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<ThingModelData>({ | ||||
|   type: ThingModelType.PROPERTY, | ||||
|   dataType: DataSpecsDataType.INT, | ||||
|   property: { | ||||
|     dataType: DataSpecsDataType.INT, | ||||
|     dataSpecs: { | ||||
|       dataType: DataSpecsDataType.INT | ||||
|     } | ||||
|   }, | ||||
|   service: {}, | ||||
|   event: {} | ||||
| }) | ||||
| 
 | ||||
| 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 ThingModelApi.getThingModel(id) | ||||
|       // 情况一:属性初始化 | ||||
|       if (isEmpty(formData.value.property)) { | ||||
|         formData.value.dataType = DataSpecsDataType.INT | ||||
|         formData.value.property = { | ||||
|           dataType: DataSpecsDataType.INT, | ||||
|           dataSpecs: { | ||||
|             dataType: DataSpecsDataType.INT | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       // 情况二:服务初始化 | ||||
|       if (isEmpty(formData.value.service)) { | ||||
|         formData.value.service = {} | ||||
|       } | ||||
|       // 情况三:事件初始化 | ||||
|       if (isEmpty(formData.value.event)) { | ||||
|         formData.value.event = {} | ||||
|       } | ||||
|     } finally { | ||||
|       formLoading.value = false | ||||
|     } | ||||
|   } | ||||
| } | ||||
| defineExpose({ open, close: () => (dialogVisible.value = false) }) | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) | ||||
| const submitForm = async () => { | ||||
|   debugger | ||||
|   await formRef.value.validate() | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     const data = cloneDeep(formData.value) as ThingModelData | ||||
|     // 信息补全 | ||||
|     data.productId = product!.value.id | ||||
|     data.productKey = product!.value.productKey | ||||
|     fillExtraAttributes(data) | ||||
|     if (formType.value === 'create') { | ||||
|       await ThingModelApi.createThingModel(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await ThingModelApi.updateThingModel(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|   } finally { | ||||
|     dialogVisible.value = false // 确保关闭弹框 | ||||
|     emit('success') | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 填写额外的属性 */ | ||||
| const fillExtraAttributes = (data: any) => { | ||||
|   // 处理不同类型的情况 | ||||
|   // 属性 | ||||
|   if (data.type === ThingModelType.PROPERTY) { | ||||
|     removeDataSpecs(data.property) | ||||
|     data.dataType = data.property.dataType | ||||
|     data.property.identifier = data.identifier | ||||
|     data.property.name = data.name | ||||
|     delete data.service | ||||
|     delete data.event | ||||
|   } | ||||
|   // 服务 | ||||
|   if (data.type === ThingModelType.SERVICE) { | ||||
|     removeDataSpecs(data.service) | ||||
|     data.dataType = data.service.dataType | ||||
|     data.service.identifier = data.identifier | ||||
|     data.service.name = data.name | ||||
|     delete data.property | ||||
|     delete data.event | ||||
|   } | ||||
|   // 事件 | ||||
|   if (data.type === ThingModelType.EVENT) { | ||||
|     removeDataSpecs(data.event) | ||||
|     data.dataType = data.event.dataType | ||||
|     data.event.identifier = data.identifier | ||||
|     data.event.name = data.name | ||||
|     delete data.property | ||||
|     delete data.service | ||||
|   } | ||||
| } | ||||
| /** 处理 dataSpecs 为空的情况 */ | ||||
| const removeDataSpecs = (val: any) => { | ||||
|   if (isEmpty(val.dataSpecs)) { | ||||
|     delete val.dataSpecs | ||||
|   } | ||||
|   if (isEmpty(val.dataSpecsList)) { | ||||
|     delete val.dataSpecsList | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     type: ThingModelType.PROPERTY, | ||||
|     dataType: DataSpecsDataType.INT, | ||||
|     property: { | ||||
|       dataType: DataSpecsDataType.INT, | ||||
|       dataSpecs: { | ||||
|         dataType: DataSpecsDataType.INT | ||||
|       } | ||||
|     }, | ||||
|     service: {}, | ||||
|     event: {} | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,155 @@ | |||
| <!-- 产品的物模型表单(event、service 项里的参数) --> | ||||
| <template> | ||||
|   <div | ||||
|     v-for="(item, index) in thingModelParams" | ||||
|     :key="index" | ||||
|     class="w-1/1 param-item flex justify-between px-10px mb-10px" | ||||
|   > | ||||
|     <span>参数名称:{{ item.name }}</span> | ||||
|     <div class="btn"> | ||||
|       <el-button link type="primary" @click="openParamForm(item)">编辑</el-button> | ||||
|       <el-divider direction="vertical" /> | ||||
|       <el-button link type="danger" @click="deleteParamItem(index)">删除</el-button> | ||||
|     </div> | ||||
|   </div> | ||||
|   <el-button link type="primary" @click="openParamForm(null)">+新增参数</el-button> | ||||
| 
 | ||||
|   <!-- param 表单 --> | ||||
|   <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body> | ||||
|     <el-form | ||||
|       ref="paramFormRef" | ||||
|       v-loading="formLoading" | ||||
|       :model="formData" | ||||
|       :rules="ThingModelFormRules" | ||||
|       label-width="100px" | ||||
|     > | ||||
|       <el-form-item label="参数名称" prop="name"> | ||||
|         <el-input v-model="formData.name" placeholder="请输入功能名称" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="标识符" prop="identifier"> | ||||
|         <el-input v-model="formData.identifier" placeholder="请输入标识符" /> | ||||
|       </el-form-item> | ||||
|       <!-- 属性配置 --> | ||||
|       <ThingModelProperty v-model="formData.property" is-params /> | ||||
|     </el-form> | ||||
| 
 | ||||
|     <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 { useVModel } from '@vueuse/core' | ||||
| import ThingModelProperty from './ThingModelProperty.vue' | ||||
| import { DataSpecsDataType, ThingModelFormRules } from './config' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** 输入输出参数配置组件 */ | ||||
| defineOptions({ name: 'ThingModelInputOutputParam' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any; direction: string }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]> | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const dialogTitle = ref('新增参数') // 弹窗的标题 | ||||
| const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||
| const paramFormRef = ref() // 表单 ref | ||||
| const formData = ref<any>({ | ||||
|   dataType: DataSpecsDataType.INT, | ||||
|   property: { | ||||
|     dataType: DataSpecsDataType.INT, | ||||
|     dataSpecs: { | ||||
|       dataType: DataSpecsDataType.INT | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| /** 打开 param 表单 */ | ||||
| const openParamForm = (val: any) => { | ||||
|   dialogVisible.value = true | ||||
|   resetForm() | ||||
|   if (isEmpty(val)) { | ||||
|     return | ||||
|   } | ||||
|   // 编辑时回显数据 | ||||
|   formData.value = { | ||||
|     identifier: val.identifier, | ||||
|     name: val.name, | ||||
|     description: val.description, | ||||
|     property: { | ||||
|       dataType: val.dataType, | ||||
|       dataSpecs: val.dataSpecs, | ||||
|       dataSpecsList: val.dataSpecsList | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 删除 param 项 */ | ||||
| const deleteParamItem = (index: number) => { | ||||
|   thingModelParams.value.splice(index, 1) | ||||
| } | ||||
| 
 | ||||
| /** 添加参数 */ | ||||
| const submitForm = async () => { | ||||
|   // 初始化参数列表 | ||||
|   if (isEmpty(thingModelParams.value)) { | ||||
|     thingModelParams.value = [] | ||||
|   } | ||||
|   // 校验参数 | ||||
|   await paramFormRef.value.validate() | ||||
|   try { | ||||
|     const data = unref(formData) | ||||
|     // 构建数据对象 | ||||
|     const item = { | ||||
|       identifier: data.identifier, | ||||
|       name: data.name, | ||||
|       description: data.description, | ||||
|       dataType: data.property.dataType, | ||||
|       paraOrder: 0, // TODO @puhui999: 先写死默认看看后续 | ||||
|       direction: props.direction, | ||||
|       dataSpecs: | ||||
|         !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1 | ||||
|           ? data.property.dataSpecs | ||||
|           : undefined, | ||||
|       dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList | ||||
|     } | ||||
| 
 | ||||
|     // 查找是否已有相同 identifier 的项 | ||||
|     const existingIndex = thingModelParams.value.findIndex( | ||||
|       (spec) => spec.identifier === data.identifier | ||||
|     ) | ||||
|     if (existingIndex > -1) { | ||||
|       // 更新已有项 | ||||
|       thingModelParams.value[existingIndex] = item | ||||
|     } else { | ||||
|       // 添加新项 | ||||
|       thingModelParams.value.push(item) | ||||
|     } | ||||
|   } finally { | ||||
|     // 隐藏对话框 | ||||
|     dialogVisible.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     dataType: DataSpecsDataType.INT, | ||||
|     property: { | ||||
|       dataType: DataSpecsDataType.INT, | ||||
|       dataSpecs: { | ||||
|         dataType: DataSpecsDataType.INT | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   paramFormRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .param-item { | ||||
|   background-color: #e4f2fd; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,169 @@ | |||
| <!-- 产品的物模型表单(property 项) --> | ||||
| <template> | ||||
|   <el-form-item | ||||
|     :rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]" | ||||
|     label="数据类型" | ||||
|     prop="property.dataType" | ||||
|   > | ||||
|     <el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange"> | ||||
|       <!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) --> | ||||
|       <el-option | ||||
|         v-for="option in getDataTypeOptions" | ||||
|         :key="option.value" | ||||
|         :label="`${option.value}(${option.label})`" | ||||
|         :value="option.value" | ||||
|       /> | ||||
|     </el-select> | ||||
|   </el-form-item> | ||||
|   <!-- 数值型配置 --> | ||||
|   <ThingModelNumberDataSpecs | ||||
|     v-if=" | ||||
|       [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes( | ||||
|         property.dataType || '' | ||||
|       ) | ||||
|     " | ||||
|     v-model="property.dataSpecs" | ||||
|   /> | ||||
|   <!-- 枚举型配置 --> | ||||
|   <ThingModelEnumDataSpecs | ||||
|     v-if="property.dataType === DataSpecsDataType.ENUM" | ||||
|     v-model="property.dataSpecsList" | ||||
|   /> | ||||
|   <!-- 布尔型配置 --> | ||||
|   <el-form-item v-if="property.dataType === DataSpecsDataType.BOOL" label="布尔值"> | ||||
|     <template v-for="(item, index) in property.dataSpecsList" :key="item.value"> | ||||
|       <div class="flex items-center justify-start w-1/1 mb-5px"> | ||||
|         <span>{{ item.value }}</span> | ||||
|         <span class="mx-2">-</span> | ||||
|         <el-form-item | ||||
|           :prop="`property.dataSpecsList[${index}].name`" | ||||
|           :rules="[ | ||||
|             { required: true, message: '枚举描述不能为空' }, | ||||
|             { validator: validateBoolName, trigger: 'blur' } | ||||
|           ]" | ||||
|           class="flex-1 mb-0" | ||||
|         > | ||||
|           <el-input | ||||
|             v-model="item.name" | ||||
|             :placeholder="`如:${item.value === 0 ? '关' : '开'}`" | ||||
|             class="w-255px!" | ||||
|           /> | ||||
|         </el-form-item> | ||||
|       </div> | ||||
|     </template> | ||||
|   </el-form-item> | ||||
|   <!-- 文本型配置 --> | ||||
|   <el-form-item | ||||
|     v-if="property.dataType === DataSpecsDataType.TEXT" | ||||
|     label="数据长度" | ||||
|     prop="property.dataSpecs.length" | ||||
|   > | ||||
|     <el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度"> | ||||
|       <template #append>字节</template> | ||||
|     </el-input> | ||||
|   </el-form-item> | ||||
|   <!-- 时间型配置 --> | ||||
|   <el-form-item v-if="property.dataType === DataSpecsDataType.DATE" label="时间格式" prop="date"> | ||||
|     <el-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" /> | ||||
|   </el-form-item> | ||||
|   <!-- 数组型配置--> | ||||
|   <ThingModelArrayDataSpecs | ||||
|     v-if="property.dataType === DataSpecsDataType.ARRAY" | ||||
|     v-model="property.dataSpecs" | ||||
|   /> | ||||
|   <!-- Struct 型配置--> | ||||
|   <ThingModelStructDataSpecs | ||||
|     v-if="property.dataType === DataSpecsDataType.STRUCT" | ||||
|     v-model="property.dataSpecsList" | ||||
|   /> | ||||
|   <el-form-item v-if="!isStructDataSpecs && !isParams" label="读写类型" prop="property.accessMode"> | ||||
|     <el-radio-group v-model="property.accessMode"> | ||||
|       <el-radio :label="ThingModelAccessMode.READ_WRITE.value"> | ||||
|         {{ ThingModelAccessMode.READ_WRITE.label }} | ||||
|       </el-radio> | ||||
|       <el-radio :label="ThingModelAccessMode.READ_ONLY.value"> | ||||
|         {{ ThingModelAccessMode.READ_ONLY.label }} | ||||
|       </el-radio> | ||||
|     </el-radio-group> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { | ||||
|   DataSpecsDataType, | ||||
|   dataTypeOptions, | ||||
|   ThingModelAccessMode, | ||||
|   validateBoolName | ||||
| } from './config' | ||||
| import { | ||||
|   ThingModelArrayDataSpecs, | ||||
|   ThingModelEnumDataSpecs, | ||||
|   ThingModelNumberDataSpecs, | ||||
|   ThingModelStructDataSpecs | ||||
| } from './dataSpecs' | ||||
| import { ThingModelProperty } from '@/api/iot/thingmodel' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** IoT 物模型属性 */ | ||||
| defineOptions({ name: 'ThingModelProperty' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean; isParams?: boolean }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty> | ||||
| const getDataTypeOptions = computed(() => { | ||||
|   return !props.isStructDataSpecs | ||||
|     ? dataTypeOptions | ||||
|     : dataTypeOptions.filter( | ||||
|         (item) => | ||||
|           !([DataSpecsDataType.STRUCT, DataSpecsDataType.ARRAY] as any[]).includes(item.value) | ||||
|       ) | ||||
| }) // 获得数据类型列表 | ||||
| 
 | ||||
| /** 属性值的数据类型切换时初始化相关数据 */ | ||||
| const handleChange = (dataType: any) => { | ||||
|   property.value.dataSpecs = {} | ||||
|   property.value.dataSpecsList = [] | ||||
|   // 不是列表型数据才设置 dataSpecs.dataType | ||||
|   ![DataSpecsDataType.ENUM, DataSpecsDataType.BOOL, DataSpecsDataType.STRUCT].includes(dataType) && | ||||
|     (property.value.dataSpecs.dataType = dataType) | ||||
|   switch (dataType) { | ||||
|     case DataSpecsDataType.ENUM: | ||||
|       property.value.dataSpecsList.push({ | ||||
|         dataType: DataSpecsDataType.ENUM, | ||||
|         name: '', // 枚举项的名称 | ||||
|         value: undefined // 枚举值 | ||||
|       }) | ||||
|       break | ||||
|     case DataSpecsDataType.BOOL: | ||||
|       for (let i = 0; i < 2; i++) { | ||||
|         property.value.dataSpecsList.push({ | ||||
|           dataType: DataSpecsDataType.BOOL, | ||||
|           name: '', // 布尔值的名称 | ||||
|           value: i // 布尔值 | ||||
|         }) | ||||
|       } | ||||
|       break | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 默认选中读写 | ||||
| watch( | ||||
|   () => property.value.accessMode, | ||||
|   (val: string) => { | ||||
|     if (props.isStructDataSpecs || props.isParams) { | ||||
|       return | ||||
|     } | ||||
|     isEmpty(val) && (property.value.accessMode = ThingModelAccessMode.READ_WRITE.value) | ||||
|   }, | ||||
|   { immediate: true } | ||||
| ) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| :deep(.el-form-item) { | ||||
|   .el-form-item { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,59 @@ | |||
| <!-- 产品的物模型表单(service 项) --> | ||||
| <template> | ||||
|   <el-form-item | ||||
|     :rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]" | ||||
|     label="调用方式" | ||||
|     prop="service.callType" | ||||
|   > | ||||
|     <el-radio-group v-model="service.callType"> | ||||
|       <el-radio :value="ThingModelServiceCallType.ASYNC.value"> | ||||
|         {{ ThingModelServiceCallType.ASYNC.label }} | ||||
|       </el-radio> | ||||
|       <el-radio :value="ThingModelServiceCallType.SYNC.value"> | ||||
|         {{ ThingModelServiceCallType.SYNC.label }} | ||||
|       </el-radio> | ||||
|     </el-radio-group> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="输入参数"> | ||||
|     <ThingModelInputOutputParam | ||||
|       v-model="service.inputParams" | ||||
|       :direction="ThingModelParamDirection.INPUT" | ||||
|     /> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="输出参数"> | ||||
|     <ThingModelInputOutputParam | ||||
|       v-model="service.outputParams" | ||||
|       :direction="ThingModelParamDirection.OUTPUT" | ||||
|     /> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue' | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { ThingModelService } from '@/api/iot/thingmodel' | ||||
| import { ThingModelParamDirection, ThingModelServiceCallType } from './config' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** IoT 物模型服务 */ | ||||
| defineOptions({ name: 'ThingModelService' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService> | ||||
| 
 | ||||
| // 默认选中,ASYNC 异步 | ||||
| watch( | ||||
|   () => service.value.callType, | ||||
|   (val: string) => isEmpty(val) && (service.value.callType = ThingModelServiceCallType.ASYNC.value), | ||||
|   { immediate: true } | ||||
| ) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| :deep(.el-form-item) { | ||||
|   .el-form-item { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,61 @@ | |||
| <template> | ||||
|   <!-- 属性 --> | ||||
|   <template v-if="data.type === ThingModelType.PROPERTY"> | ||||
|     <!-- 非列表型:数值 --> | ||||
|     <div | ||||
|       v-if=" | ||||
|         [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes( | ||||
|           data.property.dataType | ||||
|         ) | ||||
|       " | ||||
|     > | ||||
|       取值范围:{{ `${data.property.dataSpecs.min}~${data.property.dataSpecs.max}` }} | ||||
|     </div> | ||||
|     <!-- 非列表型:文本 --> | ||||
|     <div v-if="DataSpecsDataType.TEXT === data.property.dataType"> | ||||
|       数据长度:{{ data.property.dataSpecs.length }} | ||||
|     </div> | ||||
|     <!-- 列表型: 数组、结构、时间(特殊) --> | ||||
|     <div | ||||
|       v-if=" | ||||
|         [DataSpecsDataType.ARRAY, DataSpecsDataType.STRUCT, DataSpecsDataType.DATE].includes( | ||||
|           data.property.dataType | ||||
|         ) | ||||
|       " | ||||
|     > | ||||
|       - | ||||
|     </div> | ||||
|     <!-- 列表型: 布尔值、枚举 --> | ||||
|     <div v-if="[DataSpecsDataType.BOOL, DataSpecsDataType.ENUM].includes(data.property.dataType)"> | ||||
|       <div> {{ DataSpecsDataType.BOOL === data.property.dataType ? '布尔值' : '枚举值' }}:</div> | ||||
|       <div v-for="item in data.property.dataSpecsList" :key="item.value"> | ||||
|         {{ `${item.name}-${item.value}` }} | ||||
|       </div> | ||||
|     </div> | ||||
|   </template> | ||||
|   <!-- 服务 --> | ||||
|   <div v-if="data.type === ThingModelType.SERVICE"> | ||||
|     调用方式:{{ getCallTypeByValue(data.service!.callType) }} | ||||
|   </div> | ||||
|   <!-- 事件 --> | ||||
|   <div v-if="data.type === ThingModelType.EVENT"> | ||||
|     事件类型:{{ getEventTypeByValue(data.event!.type) }} | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { | ||||
|   DataSpecsDataType, | ||||
|   getCallTypeByValue, | ||||
|   getEventTypeByValue, | ||||
|   ThingModelType | ||||
| } from '@/views/iot/thingmodel/config' | ||||
| import { ThingModelData } from '@/api/iot/thingmodel' | ||||
| 
 | ||||
| /** 数据定义展示组件 */ | ||||
| defineOptions({ name: 'DataDefinition' }) | ||||
| 
 | ||||
| defineProps<{ data: ThingModelData }>() | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,3 @@ | |||
| import DataDefinition from './DataDefinition.vue' | ||||
| 
 | ||||
| export { DataDefinition } | ||||
|  | @ -0,0 +1,213 @@ | |||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** dataSpecs 数值型数据结构 */ | ||||
| export interface DataSpecsNumberDataVO { | ||||
|   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 DataSpecsEnumOrBoolDataVO { | ||||
|   dataType: 'enum' | 'bool' | ||||
|   defaultValue?: string // 默认值,可选
 | ||||
|   name: string // 枚举项的名称
 | ||||
|   value: number | undefined // 枚举值
 | ||||
| } | ||||
| 
 | ||||
| /** 属性值的数据类型 */ | ||||
| export const DataSpecsDataType = { | ||||
|   INT: 'int', | ||||
|   FLOAT: 'float', | ||||
|   DOUBLE: 'double', | ||||
|   ENUM: 'enum', | ||||
|   BOOL: 'bool', | ||||
|   TEXT: 'text', | ||||
|   DATE: 'date', | ||||
|   STRUCT: 'struct', | ||||
|   ARRAY: 'array' | ||||
| } as const | ||||
| 
 | ||||
| /** 物体模型数据类型配置项 */ | ||||
| export const dataTypeOptions = [ | ||||
|   { value: DataSpecsDataType.INT, label: '整数型' }, | ||||
|   { value: DataSpecsDataType.FLOAT, label: '单精度浮点型' }, | ||||
|   { value: DataSpecsDataType.DOUBLE, label: '双精度浮点型' }, | ||||
|   { value: DataSpecsDataType.ENUM, label: '枚举型' }, | ||||
|   { value: DataSpecsDataType.BOOL, label: '布尔型' }, | ||||
|   { value: DataSpecsDataType.TEXT, label: '文本型' }, | ||||
|   { value: DataSpecsDataType.DATE, label: '时间型' }, | ||||
|   { value: DataSpecsDataType.STRUCT, label: '结构体' }, | ||||
|   { value: DataSpecsDataType.ARRAY, label: '数组' } | ||||
| ] | ||||
| 
 | ||||
| /** 获得物体模型数据类型配置项名称 */ | ||||
| export const getDataTypeOptionsLabel = (value: string) => { | ||||
|   if (isEmpty(value)) { | ||||
|     return value | ||||
|   } | ||||
|   const dataType = dataTypeOptions.find((option) => option.value === value) | ||||
|   return dataType && `${dataType.value}(${dataType.label})` | ||||
| } | ||||
| 
 | ||||
| // IOT 产品物模型类型枚举类
 | ||||
| export const ThingModelType = { | ||||
|   PROPERTY: 1, // 属性
 | ||||
|   SERVICE: 2, // 服务
 | ||||
|   EVENT: 3 // 事件
 | ||||
| } as const | ||||
| 
 | ||||
| // IOT 产品物模型访问模式枚举类
 | ||||
| export const ThingModelAccessMode = { | ||||
|   READ_WRITE: { | ||||
|     label: '读写', | ||||
|     value: 'rw' | ||||
|   }, | ||||
|   READ_ONLY: { | ||||
|     label: '只读', | ||||
|     value: 'r' | ||||
|   } | ||||
| } as const | ||||
| 
 | ||||
| // IOT 产品物模型服务调用方式枚举
 | ||||
| export const ThingModelServiceCallType = { | ||||
|   ASYNC: { | ||||
|     label: '异步调用', | ||||
|     value: 'async' | ||||
|   }, | ||||
|   SYNC: { | ||||
|     label: '同步调用', | ||||
|     value: 'sync' | ||||
|   } | ||||
| } as const | ||||
| export const getCallTypeByValue = (value: string): string | undefined => | ||||
|   Object.values(ThingModelServiceCallType).find((type) => type.value === value)?.label | ||||
| 
 | ||||
| // IOT 产品物模型事件类型枚举
 | ||||
| export const ThingModelEventType = { | ||||
|   INFO: { | ||||
|     label: '信息', | ||||
|     value: 'info' | ||||
|   }, | ||||
|   ALERT: { | ||||
|     label: '告警', | ||||
|     value: 'alert' | ||||
|   }, | ||||
|   ERROR: { | ||||
|     label: '故障', | ||||
|     value: 'error' | ||||
|   } | ||||
| } as const | ||||
| export const getEventTypeByValue = (value: string): string | undefined => | ||||
|   Object.values(ThingModelEventType).find((type) => type.value === value)?.label | ||||
| 
 | ||||
| // IOT 产品物模型参数是输入参数还是输出参数
 | ||||
| export const ThingModelParamDirection = { | ||||
|   INPUT: 'input', // 输入参数
 | ||||
|   OUTPUT: 'output' // 输出参数
 | ||||
| } as const | ||||
| 
 | ||||
| /** 公共校验规则 */ | ||||
| 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() | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| <!-- dataType:array 数组类型 --> | ||||
| <template> | ||||
|   <el-form-item label="元素类型" prop="property.dataSpecs.childDataType"> | ||||
|     <el-radio-group v-model="dataSpecs.childDataType" @change="handleChange"> | ||||
|       <template v-for="item in dataTypeOptions" :key="item.value"> | ||||
|         <el-radio | ||||
|           v-if=" | ||||
|             !( | ||||
|               [DataSpecsDataType.ENUM, DataSpecsDataType.ARRAY, DataSpecsDataType.DATE] as any[] | ||||
|             ).includes(item.value) | ||||
|           " | ||||
|           :value="item.value" | ||||
|           class="w-1/3" | ||||
|         > | ||||
|           {{ `${item.value}(${item.label})` }} | ||||
|         </el-radio> | ||||
|       </template> | ||||
|     </el-radio-group> | ||||
|   </el-form-item> | ||||
|   <el-form-item label="元素个数" prop="property.dataSpecs.size"> | ||||
|     <el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" /> | ||||
|   </el-form-item> | ||||
|   <!-- Struct 型配置--> | ||||
|   <ThingModelStructDataSpecs | ||||
|     v-if="dataSpecs.childDataType === DataSpecsDataType.STRUCT" | ||||
|     v-model="dataSpecs.dataSpecsList" | ||||
|   /> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { DataSpecsDataType, dataTypeOptions } from '../config' | ||||
| import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue' | ||||
| 
 | ||||
| /** 数组型的 dataSpecs 配置组件 */ | ||||
| defineOptions({ name: 'ThingModelArrayDataSpecs' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any> | ||||
| 
 | ||||
| /** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */ | ||||
| const handleChange = (val: string) => { | ||||
|   if (val !== DataSpecsDataType.STRUCT) { | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   dataSpecs.value.dataSpecsList = [] | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,159 @@ | |||
| <!-- dataType:enum 数组类型 --> | ||||
| <template> | ||||
|   <el-form-item | ||||
|     :rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]" | ||||
|     label="枚举项" | ||||
|   > | ||||
|     <div class="flex flex-col"> | ||||
|       <div class="flex items-center"> | ||||
|         <span class="flex-1"> 参数值 </span> | ||||
|         <span class="flex-1"> 参数描述 </span> | ||||
|       </div> | ||||
|       <div | ||||
|         v-for="(item, index) in dataSpecsList" | ||||
|         :key="index" | ||||
|         class="flex items-center justify-between mb-5px" | ||||
|       > | ||||
|         <el-form-item | ||||
|           :prop="`property.dataSpecsList[${index}].value`" | ||||
|           :rules="[ | ||||
|             { required: true, message: '枚举值不能为空' }, | ||||
|             { validator: validateEnumValue, trigger: 'blur' } | ||||
|           ]" | ||||
|           class="flex-1 mb-0" | ||||
|         > | ||||
|           <el-input v-model="item.value" placeholder="请输入枚举值,如'0'" /> | ||||
|         </el-form-item> | ||||
|         <span class="mx-2">~</span> | ||||
|         <el-form-item | ||||
|           :prop="`property.dataSpecsList[${index}].name`" | ||||
|           :rules="[ | ||||
|             { required: true, message: '枚举描述不能为空' }, | ||||
|             { validator: validateEnumName, trigger: 'blur' } | ||||
|           ]" | ||||
|           class="flex-1 mb-0" | ||||
|         > | ||||
|           <el-input v-model="item.name" placeholder="对该枚举项的描述" /> | ||||
|         </el-form-item> | ||||
|         <el-button class="ml-10px" link type="primary" @click="deleteEnum(index)">删除</el-button> | ||||
|       </div> | ||||
|       <el-button link type="primary" @click="addEnum">+添加枚举项</el-button> | ||||
|     </div> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { DataSpecsDataType, DataSpecsEnumOrBoolDataVO } from '../config' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** 枚举型的 dataSpecs 配置组件 */ | ||||
| defineOptions({ name: 'ThingModelEnumDataSpecs' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolDataVO[]> | ||||
| const message = useMessage() | ||||
| 
 | ||||
| /** 添加枚举项 */ | ||||
| const addEnum = () => { | ||||
|   dataSpecsList.value.push({ | ||||
|     dataType: DataSpecsDataType.ENUM, | ||||
|     name: '', // 枚举项的名称 | ||||
|     value: undefined // 枚举值 | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 删除枚举项 */ | ||||
| const deleteEnum = (index: number) => { | ||||
|   if (dataSpecsList.value.length === 1) { | ||||
|     message.warning('至少需要一个枚举项') | ||||
|     return | ||||
|   } | ||||
|   dataSpecsList.value.splice(index, 1) | ||||
| } | ||||
| 
 | ||||
| /** 校验枚举值 */ | ||||
| const validateEnumValue = (_: any, value: any, callback: any) => { | ||||
|   if (isEmpty(value)) { | ||||
|     callback(new Error('枚举值不能为空')) | ||||
|     return | ||||
|   } | ||||
|   if (isNaN(Number(value))) { | ||||
|     callback(new Error('枚举值必须是数字')) | ||||
|     return | ||||
|   } | ||||
|   // 检查枚举值是否重复 | ||||
|   const values = dataSpecsList.value.map((item) => item.value) | ||||
|   if (values.filter((v) => v === value).length > 1) { | ||||
|     callback(new Error('枚举值不能重复')) | ||||
|     return | ||||
|   } | ||||
|   callback() | ||||
| } | ||||
| 
 | ||||
| /** 校验枚举描述 */ | ||||
| const validateEnumName = (_: 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() | ||||
| } | ||||
| 
 | ||||
| /** 校验整个枚举列表 */ | ||||
| const validateEnumList = (_: any, __: any, callback: any) => { | ||||
|   if (isEmpty(dataSpecsList.value)) { | ||||
|     callback(new Error('请至少添加一个枚举项')) | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   // 检查是否存在空值 | ||||
|   const hasEmptyValue = dataSpecsList.value.some( | ||||
|     (item) => isEmpty(item.value) || isEmpty(item.name) | ||||
|   ) | ||||
|   if (hasEmptyValue) { | ||||
|     callback(new Error('存在未填写的枚举值或描述')) | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   // 检查枚举值是否都是数字 | ||||
|   const hasInvalidNumber = dataSpecsList.value.some((item) => isNaN(Number(item.value))) | ||||
|   if (hasInvalidNumber) { | ||||
|     callback(new Error('存在非数字的枚举值')) | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   // 检查是否有重复的枚举值 | ||||
|   const values = dataSpecsList.value.map((item) => item.value) | ||||
|   const uniqueValues = new Set(values) | ||||
|   if (values.length !== uniqueValues.size) { | ||||
|     callback(new Error('存在重复的枚举值')) | ||||
|     return | ||||
|   } | ||||
|   callback() | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| :deep(.el-form-item) { | ||||
|   .el-form-item { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,139 @@ | |||
| <!-- dataType:number 数组类型 --> | ||||
| <template> | ||||
|   <el-form-item label="取值范围"> | ||||
|     <div class="flex items-center justify-between"> | ||||
|       <el-form-item | ||||
|         :rules="[ | ||||
|           { required: true, message: '最小值不能为空' }, | ||||
|           { validator: validateMin, trigger: 'blur' } | ||||
|         ]" | ||||
|         class="mb-0" | ||||
|         prop="property.dataSpecs.min" | ||||
|       > | ||||
|         <el-input v-model="dataSpecs.min" placeholder="请输入最小值" /> | ||||
|       </el-form-item> | ||||
|       <span class="mx-2">~</span> | ||||
|       <el-form-item | ||||
|         :rules="[ | ||||
|           { required: true, message: '最大值不能为空' }, | ||||
|           { validator: validateMax, trigger: 'blur' } | ||||
|         ]" | ||||
|         class="mb-0" | ||||
|         prop="property.dataSpecs.max" | ||||
|       > | ||||
|         <el-input v-model="dataSpecs.max" placeholder="请输入最大值" /> | ||||
|       </el-form-item> | ||||
|     </div> | ||||
|   </el-form-item> | ||||
|   <el-form-item | ||||
|     :rules="[ | ||||
|       { required: true, message: '步长不能为空' }, | ||||
|       { validator: validateStep, trigger: 'blur' } | ||||
|     ]" | ||||
|     label="步长" | ||||
|     prop="property.dataSpecs.step" | ||||
|   > | ||||
|     <el-input v-model="dataSpecs.step" placeholder="请输入步长" /> | ||||
|   </el-form-item> | ||||
|   <el-form-item | ||||
|     :rules="[{ required: true, message: '请选择单位' }]" | ||||
|     label="单位" | ||||
|     prop="property.dataSpecs.unit" | ||||
|   > | ||||
|     <el-select | ||||
|       :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''" | ||||
|       filterable | ||||
|       placeholder="请选择单位" | ||||
|       class="w-1/1" | ||||
|       @change="unitChange" | ||||
|     > | ||||
|       <el-option | ||||
|         v-for="(item, index) in getStrDictOptions(DICT_TYPE.IOT_THING_MODEL_UNIT)" | ||||
|         :key="index" | ||||
|         :label="item.label + '-' + item.value" | ||||
|         :value="item.label + '-' + item.value" | ||||
|       /> | ||||
|     </el-select> | ||||
|   </el-form-item> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { useVModel } from '@vueuse/core' | ||||
| import { DataSpecsNumberDataVO } from '../config' | ||||
| import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' | ||||
| 
 | ||||
| /** 数值型的 dataSpecs 配置组件 */ | ||||
| defineOptions({ name: 'ThingModelNumberDataSpecs' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<DataSpecsNumberDataVO> | ||||
| 
 | ||||
| /** 单位发生变化时触发 */ | ||||
| const unitChange = (UnitSpecs: string) => { | ||||
|   const [unitName, unit] = UnitSpecs.split('-') | ||||
|   dataSpecs.value.unitName = unitName | ||||
|   dataSpecs.value.unit = unit | ||||
| } | ||||
| 
 | ||||
| /** 校验最小值 */ | ||||
| const validateMin = (_: any, __: any, callback: any) => { | ||||
|   const min = Number(dataSpecs.value.min) | ||||
|   const max = Number(dataSpecs.value.max) | ||||
|   if (isNaN(min)) { | ||||
|     callback(new Error('请输入有效的数值')) | ||||
|     return | ||||
|   } | ||||
|   if (max !== undefined && !isNaN(max) && min >= max) { | ||||
|     callback(new Error('最小值必须小于最大值')) | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   callback() | ||||
| } | ||||
| 
 | ||||
| /** 校验最大值 */ | ||||
| const validateMax = (_: any, __: any, callback: any) => { | ||||
|   const min = Number(dataSpecs.value.min) | ||||
|   const max = Number(dataSpecs.value.max) | ||||
|   if (isNaN(max)) { | ||||
|     callback(new Error('请输入有效的数值')) | ||||
|     return | ||||
|   } | ||||
|   if (min !== undefined && !isNaN(min) && max <= min) { | ||||
|     callback(new Error('最大值必须大于最小值')) | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   callback() | ||||
| } | ||||
| 
 | ||||
| /** 校验步长 */ | ||||
| const validateStep = (_: any, __: any, callback: any) => { | ||||
|   const step = Number(dataSpecs.value.step) | ||||
|   if (isNaN(step)) { | ||||
|     callback(new Error('请输入有效的数值')) | ||||
|     return | ||||
|   } | ||||
|   if (step <= 0) { | ||||
|     callback(new Error('步长必须大于0')) | ||||
|     return | ||||
|   } | ||||
|   const min = Number(dataSpecs.value.min) | ||||
|   const max = Number(dataSpecs.value.max) | ||||
|   if (!isNaN(min) && !isNaN(max) && step > max - min) { | ||||
|     callback(new Error('步长不能大于最大值和最小值的差值')) | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   callback() | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| :deep(.el-form-item) { | ||||
|   .el-form-item { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,170 @@ | |||
| <!-- dataType:struct 数组类型 --> | ||||
| <template> | ||||
|   <!-- struct 数据展示 --> | ||||
|   <el-form-item | ||||
|     :rules="[{ required: true, validator: validateList, trigger: 'change' }]" | ||||
|     label="JSON 对象" | ||||
|   > | ||||
|     <div | ||||
|       v-for="(item, index) in dataSpecsList" | ||||
|       :key="index" | ||||
|       class="w-1/1 struct-item flex justify-between px-10px mb-10px" | ||||
|     > | ||||
|       <span>参数名称:{{ item.name }}</span> | ||||
|       <div class="btn"> | ||||
|         <el-button link type="primary" @click="openStructForm(item)">编辑</el-button> | ||||
|         <el-divider direction="vertical" /> | ||||
|         <el-button link type="danger" @click="deleteStructItem(index)">删除</el-button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <el-button link type="primary" @click="openStructForm(null)">+新增参数</el-button> | ||||
|   </el-form-item> | ||||
| 
 | ||||
|   <!-- struct 表单 --> | ||||
|   <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body> | ||||
|     <el-form | ||||
|       ref="structFormRef" | ||||
|       v-loading="formLoading" | ||||
|       :model="formData" | ||||
|       :rules="ThingModelFormRules" | ||||
|       label-width="100px" | ||||
|     > | ||||
|       <el-form-item label="参数名称" prop="name"> | ||||
|         <el-input v-model="formData.name" placeholder="请输入功能名称" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="标识符" prop="identifier"> | ||||
|         <el-input v-model="formData.identifier" placeholder="请输入标识符" /> | ||||
|       </el-form-item> | ||||
|       <!-- 属性配置 --> | ||||
|       <ThingModelProperty v-model="formData.property" is-struct-data-specs /> | ||||
|     </el-form> | ||||
| 
 | ||||
|     <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 { useVModel } from '@vueuse/core' | ||||
| import ThingModelProperty from '../ThingModelProperty.vue' | ||||
| import { DataSpecsDataType, ThingModelFormRules } from '../config' | ||||
| import { isEmpty } from '@/utils/is' | ||||
| 
 | ||||
| /** Struct 型的 dataSpecs 配置组件 */ | ||||
| defineOptions({ name: 'ThingModelStructDataSpecs' }) | ||||
| 
 | ||||
| const props = defineProps<{ modelValue: any }>() | ||||
| const emits = defineEmits(['update:modelValue']) | ||||
| const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]> | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const dialogTitle = ref('新增参数') // 弹窗的标题 | ||||
| const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||
| const structFormRef = ref() // 表单 ref | ||||
| const formData = ref<any>({ | ||||
|   property: { | ||||
|     dataType: DataSpecsDataType.INT, | ||||
|     dataSpecs: { | ||||
|       dataType: DataSpecsDataType.INT | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| /** 打开 struct 表单 */ | ||||
| const openStructForm = (val: any) => { | ||||
|   dialogVisible.value = true | ||||
|   resetForm() | ||||
|   if (isEmpty(val)) { | ||||
|     return | ||||
|   } | ||||
|   // 编辑时回显数据 | ||||
|   formData.value = { | ||||
|     identifier: val.identifier, | ||||
|     name: val.name, | ||||
|     description: val.description, | ||||
|     property: { | ||||
|       dataType: val.childDataType, | ||||
|       dataSpecs: val.dataSpecs, | ||||
|       dataSpecsList: val.dataSpecsList | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 删除 struct 项 */ | ||||
| const deleteStructItem = (index: number) => { | ||||
|   dataSpecsList.value.splice(index, 1) | ||||
| } | ||||
| 
 | ||||
| /** 添加参数 */ | ||||
| const submitForm = async () => { | ||||
|   await structFormRef.value.validate() | ||||
| 
 | ||||
|   try { | ||||
|     const data = unref(formData) | ||||
|     // 构建数据对象 | ||||
|     const item = { | ||||
|       identifier: data.identifier, | ||||
|       name: data.name, | ||||
|       description: data.description, | ||||
|       dataType: DataSpecsDataType.STRUCT, | ||||
|       childDataType: data.property.dataType, | ||||
|       dataSpecs: | ||||
|         !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1 | ||||
|           ? data.property.dataSpecs | ||||
|           : undefined, | ||||
|       dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList | ||||
|     } | ||||
| 
 | ||||
|     // 查找是否已有相同 identifier 的项 | ||||
|     const existingIndex = dataSpecsList.value.findIndex( | ||||
|       (spec) => spec.identifier === data.identifier | ||||
|     ) | ||||
|     if (existingIndex > -1) { | ||||
|       // 更新已有项 | ||||
|       dataSpecsList.value[existingIndex] = item | ||||
|     } else { | ||||
|       // 添加新项 | ||||
|       dataSpecsList.value.push(item) | ||||
|     } | ||||
|   } finally { | ||||
|     // 隐藏对话框 | ||||
|     dialogVisible.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     property: { | ||||
|       dataType: DataSpecsDataType.INT, | ||||
|       dataSpecs: { | ||||
|         dataType: DataSpecsDataType.INT | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   structFormRef.value?.resetFields() | ||||
| } | ||||
| 
 | ||||
| /** 校验 struct 不能为空 */ | ||||
| const validateList = (_: any, __: any, callback: any) => { | ||||
|   if (isEmpty(dataSpecsList.value)) { | ||||
|     callback(new Error('struct 不能为空')) | ||||
|     return | ||||
|   } | ||||
|   callback() | ||||
| } | ||||
| 
 | ||||
| /** 组件初始化 */ | ||||
| onMounted(async () => { | ||||
|   await nextTick() | ||||
|   // 预防 dataSpecsList 空指针 | ||||
|   isEmpty(dataSpecsList.value) && (dataSpecsList.value = []) | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .struct-item { | ||||
|   background-color: #e4f2fd; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,11 @@ | |||
| import ThingModelEnumDataSpecs from './ThingModelEnumDataSpecs.vue' | ||||
| import ThingModelNumberDataSpecs from './ThingModelNumberDataSpecs.vue' | ||||
| import ThingModelArrayDataSpecs from './ThingModelArrayDataSpecs.vue' | ||||
| import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue' | ||||
| 
 | ||||
| export { | ||||
|   ThingModelEnumDataSpecs, | ||||
|   ThingModelNumberDataSpecs, | ||||
|   ThingModelArrayDataSpecs, | ||||
|   ThingModelStructDataSpecs | ||||
| } | ||||
|  | @ -1,22 +1,23 @@ | |||
| <!-- 产品的物模型列表 --> | ||||
| <template> | ||||
|   <ContentWrap> | ||||
|     <!-- 搜索工作栏 --> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       :model="queryParams" | ||||
|       class="-mb-15px" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="功能类型" prop="name"> | ||||
|         <el-select | ||||
|           v-model="queryParams.type" | ||||
|           placeholder="请选择功能类型" | ||||
|           clearable | ||||
|           class="!w-240px" | ||||
|           clearable | ||||
|           placeholder="请选择功能类型" | ||||
|         > | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE)" | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|  | @ -24,44 +25,63 @@ | |||
|         </el-select> | ||||
|       </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 @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-button | ||||
|           type="primary" | ||||
|           v-hasPermi="[`iot:thing-model:create`]" | ||||
|           plain | ||||
|           type="primary" | ||||
|           @click="openForm('create')" | ||||
|           v-hasPermi="['iot:think-model-function:create']" | ||||
|         > | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 添加功能 | ||||
|           <Icon class="mr-5px" icon="ep:plus" /> | ||||
|           添加功能 | ||||
|         </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="type"> | ||||
|       <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> | ||||
|         <el-table-column align="center" label="功能类型" prop="type"> | ||||
|           <template #default="scope"> | ||||
|             <dict-tag :type="DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE" :value="scope.row.type" /> | ||||
|             <dict-tag :type="DICT_TYPE.IOT_THING_MODEL_TYPE" :value="scope.row.type" /> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column label="功能名称" align="center" prop="name" /> | ||||
|         <el-table-column label="标识符" align="center" prop="identifier" /> | ||||
|         <el-table-column label="操作" align="center"> | ||||
|         <el-table-column align="center" label="功能名称" prop="name" /> | ||||
|         <el-table-column align="center" label="标识符" prop="identifier" /> | ||||
|         <el-table-column align="center" label="数据类型" prop="identifier"> | ||||
|           <template #default="{ row }"> | ||||
|             {{ dataTypeOptionsLabel(row.property?.dataType) ?? '-' }} | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column align="left" label="数据定义" prop="identifier"> | ||||
|           <template #default="{ row }"> | ||||
|             <DataDefinition :data="row" /> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column align="center" label="操作"> | ||||
|           <template #default="scope"> | ||||
|             <el-button | ||||
|               v-hasPermi="[`iot:thing-model:update`]" | ||||
|               link | ||||
|               type="primary" | ||||
|               @click="openForm('update', scope.row.id)" | ||||
|               v-hasPermi="[`iot:think-model-function:update`]" | ||||
|             > | ||||
|               编辑 | ||||
|             </el-button> | ||||
|             <el-button | ||||
|               v-hasPermi="['iot:thing-model:delete']" | ||||
|               link | ||||
|               type="danger" | ||||
|               @click="handleDelete(scope.row.id)" | ||||
|               v-hasPermi="['iot:think-model-function:delete']" | ||||
|             > | ||||
|               删除 | ||||
|             </el-button> | ||||
|  | @ -70,29 +90,32 @@ | |||
|       </el-table> | ||||
|       <!-- 分页 --> | ||||
|       <Pagination | ||||
|         :total="total" | ||||
|         v-model:page="queryParams.pageNo" | ||||
|         v-model:limit="queryParams.pageSize" | ||||
|         v-model:page="queryParams.pageNo" | ||||
|         :total="total" | ||||
|         @pagination="getList" | ||||
|       /> | ||||
|     </el-tabs> | ||||
|   </ContentWrap> | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <ThinkModelFunctionForm ref="formRef" :product="product" @success="getList" /> | ||||
|   <ThingModelForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { ProductVO } from '@/api/iot/product' | ||||
| import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction' | ||||
| <script lang="ts" setup> | ||||
| import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel' | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import ThinkModelFunctionForm from '@/views/iot/product/detail/ThinkModelFunctionForm.vue' | ||||
| import ThingModelForm from './ThingModelForm.vue' | ||||
| import { ProductVO } from '@/api/iot/product/product' | ||||
| import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants' | ||||
| import { getDataTypeOptionsLabel } from './config' | ||||
| import { DataDefinition } from './components' | ||||
| 
 | ||||
| const props = defineProps<{ product: ProductVO }>() | ||||
| defineOptions({ name: 'IoTThingModel' }) | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<ThinkModelFunctionVO[]>([]) // 列表的数据 | ||||
| const list = ref<ThingModelData[]>([]) // 列表的数据 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|  | @ -102,19 +125,22 @@ const queryParams = reactive({ | |||
| }) | ||||
| 
 | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息 | ||||
| const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     queryParams.productId = props.product.id | ||||
|     const data = await ThinkModelFunctionApi.getThinkModelFunctionPage(queryParams) | ||||
|     queryParams.productId = product?.value?.id || -1 | ||||
|     const data = await ThingModelApi.getThingModelPage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|  | @ -140,7 +166,7 @@ const handleDelete = async (id: number) => { | |||
|     // 删除的二次确认 | ||||
|     await message.delConfirm() | ||||
|     // 发起删除 | ||||
|     await ThinkModelFunctionApi.deleteThinkModelFunction(id) | ||||
|     await ThingModelApi.deleteThingModel(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|  | @ -0,0 +1,4 @@ | |||
| /** iot 依赖注入 KEY */ | ||||
| export const IOT_PROVIDE_KEY = { | ||||
|   PRODUCT: 'IOT_PRODUCT' | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	 芋道源码
						芋道源码