Commit 20a2ced4 by 芋道源码 Committed by Gitee

!738 【代码评审】IoT:支持设备管理等功能

Merge pull request !738 from 芋道源码/feature/iot
parents 41f2c719 e29d6f91
......@@ -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",
......
......@@ -10,10 +10,9 @@ export interface DeviceVO {
deviceType: number // 设备类型
nickname: string // 设备备注名称
gatewayId: number // 网关设备 ID
status: number // 设备状态
statusLastUpdateTime: Date // 设备状态最后更新时间
lastOnlineTime: Date // 最后上线时间
lastOfflineTime: Date // 最后离线时间
state: number // 设备状态
onlineTime: Date // 最后上线时间
offlineTime: Date // 最后离线时间
activeTime: Date // 设备激活时间
createTime: Date // 创建时间
ip: string // 设备的 IP 地址
......@@ -28,11 +27,57 @@ export interface DeviceVO {
areaId: number // 地区编码
address: string // 设备详细地址
serialNumber: string // 设备序列号
config: string // 设备配置
groupIds?: number[] // 添加分组 ID
}
export interface DeviceUpdateStatusVO {
id: number // 设备 ID,主键,自增
status: number // 设备状态
// 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
......@@ -57,18 +102,68 @@ export const DeviceApi = {
return await request.put({ url: `/iot/device/update`, data })
},
// 修改设备状态
updateDeviceStatus: async (data: DeviceUpdateStatusVO) => {
return await request.put({ url: `/iot/device/update-status`, 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 } })
}
}
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` })
}
}
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 })
}
}
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' })
}
}
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 })
}
}
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
})
}
}
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 })
}
}
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 })
}
}
<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>
\ No newline at end of file
<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>
\ No newline at end of file
......@@ -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/device/detail/index.vue')
},
{
path: 'plugin/detail/:id',
name: 'IoTPluginDetail',
meta: {
title: '插件详情',
noCache: true,
hidden: true,
activeMenu: '/iot/plugin'
},
component: () => import('@/views/iot/device/detail/index.vue')
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' // 桥梁类型
}
......@@ -117,6 +117,20 @@ export function toAnyString() {
}
/**
* 生成指定长度的随机字符串
*
* @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']
......@@ -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 ''
}
}
......
<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>
<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>
<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>
<!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)-->
<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>
<!-- 设备配置 -->
<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>
<!-- 设备信息(头部) -->
<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 } })
}
......
<!-- 设备信息 -->
<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">
......@@ -18,23 +18,23 @@
<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.lastOnlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="当前状态">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.status" />
{{ formatDate(device.onlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后离线时间" :span="3">
{{ formatDate(device.lastOfflineTime) }}
{{ formatDate(device.offlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="MQTT 连接参数">
<el-button type="primary" @click="openMqttParams">查看</el-button>
</el-descriptions-item>
</el-descriptions>
</el-collapse>
</ContentWrap>
<!-- MQTT 连接参数弹框 -->
<Dialog
......@@ -63,8 +63,15 @@
</el-input>
</el-form-item>
<el-form-item label="passwd">
<el-input v-model="mqttParams.mqttPassword" readonly type="password">
<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>
......@@ -76,23 +83,24 @@
<el-button @click="mqttDialogVisible = false">关闭</el-button>
</template>
</Dialog>
</ContentWrap>
<!-- TODO 待开发:设备标签 -->
<!-- TODO 待开发:设备地图 -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { ProductVO } from '@/api/iot/product'
import { ProductVO } from '@/api/iot/product/product'
import { formatDate } from '@/utils/formatTime'
import { DeviceVO } from '@/api/iot/device'
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 activeNames = ref(['basicInfo']) // 展示的折叠面板
const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性
const passwordVisible = ref(false) // 定义密码可见性状态
const mqttParams = ref({
mqttClientId: '',
mqttUsername: '',
......@@ -100,20 +108,33 @@ const mqttParams = ref({
}) // 定义 MQTT 参数对象
/** 复制到剪贴板方法 */
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('复制失败')
}
}
/** 打开 MQTT 参数弹框的方法 */
const openMqttParams = () => {
const openMqttParams = async () => {
try {
const data = await DeviceApi.getMqttConnectionParams(device.id)
// 根据 API 响应结构正确获取数据
// TODO @haohao:'N/A' 是不是在 ui 里处理哈
mqttParams.value = {
mqttClientId: device.mqttClientId || 'N/A',
mqttUsername: device.mqttUsername || 'N/A',
mqttPassword: device.mqttPassword || 'N/A'
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 弹框的方法 */
......
<!-- 设备日志 -->
<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>
<!-- 设备物模型:运行状态(属性)、事件管理、服务调用 -->
<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>
......@@ -6,38 +6,62 @@
@refresh="getDeviceData(id)"
/>
<el-col>
<el-tabs>
<el-tab-pane label="设备信息">
<DeviceDetailsInfo :product="product" :device="device" />
<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="物模型数据" />
<el-tab-pane label="子设备管理" />
<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'
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'
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 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 (id: number) => {
const getDeviceData = async () => {
loading.value = true
try {
device.value = await DeviceApi.getDevice(id)
console.log(product.value)
await getProductData(device.value.productId)
} finally {
loading.value = false
......@@ -47,11 +71,8 @@ const getDeviceData = async (id: number) => {
/** 获取产品详情 */
const getProductData = async (id: number) => {
product.value = await ProductApi.getProduct(id)
console.log(product.value)
}
/** 获取物模型 */
/** 初始化 */
const { delView } = useTagsViewStore() // 视图操作
const { currentRoute } = useRouter() // 路由
......@@ -61,6 +82,7 @@ onMounted(async () => {
delView(unref(currentRoute))
return
}
await getDeviceData(id)
await getDeviceData()
activeTab.value = (route.query.tab as string) || 'info'
})
</script>
<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">
<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">
<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">
<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()
......
......@@ -7,31 +7,19 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="产品" prop="productId">
<el-select
v-model="formData.productId"
placeholder="请选择产品"
:disabled="formType === 'update'"
clearable
>
<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="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
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-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>
......@@ -40,11 +28,11 @@
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device'
import { ProductApi } from '@/api/iot/product'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { PluginConfigApi, PluginConfigVO } from '@/api/iot/plugin'
/** IoT 设备 表单 */
defineOptions({ name: 'IoTDeviceForm' })
/** IoT 插件配置 表单 */
defineOptions({ name: 'PluginConfigForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
......@@ -55,39 +43,12 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
productId: undefined,
deviceName: undefined,
nickname: undefined
name: undefined,
deployType: 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'
}
]
name: [{ required: true, message: '插件名称不能为空', trigger: 'blur' }],
deployType: [{ required: true, message: '部署方式不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
......@@ -101,7 +62,7 @@ const open = async (type: string, id?: number) => {
if (id) {
formLoading.value = true
try {
formData.value = await DeviceApi.getDevice(id)
formData.value = await PluginConfigApi.getPluginConfig(id)
} finally {
formLoading.value = false
}
......@@ -117,12 +78,12 @@ const submitForm = async () => {
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as DeviceVO
const data = formData.value as unknown as PluginConfigVO
if (formType.value === 'create') {
await DeviceApi.createDevice(data)
await PluginConfigApi.createPluginConfig(data)
message.success(t('common.createSuccess'))
} else {
await DeviceApi.updateDevice(data)
await PluginConfigApi.updatePluginConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
......@@ -137,20 +98,9 @@ const submitForm = async () => {
const resetForm = () => {
formData.value = {
id: undefined,
productId: undefined,
deviceName: undefined,
nickname: undefined
name: undefined,
deployType: undefined
}
formRef.value?.resetFields()
}
/** 查询字典下拉列表 */
const products = ref()
const getProducts = async () => {
products.value = await ProductApi.getSimpleProductList()
}
onMounted(() => {
getProducts()
})
</script>
<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>
<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>
<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>
<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>
<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-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)
......
<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="所属分类">{{ product.categoryName }}</el-descriptions-item>
<el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item>
......@@ -20,25 +20,24 @@
</el-descriptions-item>
<el-descriptions-item
label="联网方式"
v-if="product.deviceType === 0 || product.deviceType === 2"
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 === 1">
<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>
</el-collapse>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { ProductVO } from '@/api/iot/product'
import { DeviceTypeEnum, ProductVO } from '@/api/iot/product/product'
import { formatDate } from '@/utils/formatTime'
const { product } = defineProps<{ product: ProductVO }>()
// 展示的折叠面板
const activeNames = ref(['basicInfo'])
</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([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
// TODO 芋艿:不确定未来会不会改,所以先写死
const columns2 = reactive([
// 基础通信 Topic 列
const basicColumn = 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>
<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>
<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>
<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>
<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>
<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>
<!-- 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>
<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>
<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>
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
}
<!-- 产品的物模型表单(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>
<!-- 产品的物模型表单 -->
<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>
<!-- 产品的物模型表单(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>
<!-- 产品的物模型表单(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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment