Commit 2020687a by licheng

chore: 删除iot模块

parent 0ff65517
import request from '@/config/axios'
// IoT 设备 VO
export interface DeviceVO {
id: number // 设备 ID,主键,自增
deviceKey: string // 设备唯一标识符
deviceName: string // 设备名称
productId: number // 产品编号
productKey: string // 产品标识
deviceType: number // 设备类型
nickname: string // 设备备注名称
gatewayId: number // 网关设备 ID
state: number // 设备状态
onlineTime: Date // 最后上线时间
offlineTime: Date // 最后离线时间
activeTime: Date // 设备激活时间
createTime: Date // 创建时间
ip: string // 设备的 IP 地址
firmwareVersion: string // 设备的固件版本
deviceSecret: string // 设备密钥,用于设备认证,需安全存储
mqttClientId: string // MQTT 客户端 ID
mqttUsername: string // MQTT 用户名
mqttPassword: string // MQTT 密码
authType: string // 认证类型
latitude: number // 设备位置的纬度
longitude: number // 设备位置的经度
areaId: number // 地区编码
address: string // 设备详细地址
serialNumber: string // 设备序列号
config: string // 设备配置
groupIds?: number[] // 添加分组 ID
}
// IoT 设备数据 VO
export interface DeviceDataVO {
deviceId: number // 设备编号
thinkModelFunctionId: number // 物模型编号
productKey: string // 产品标识
deviceName: string // 设备名称
identifier: string // 属性标识符
name: string // 属性名称
dataType: string // 数据类型
updateTime: Date // 更新时间
value: string // 最新值
}
// IoT 设备数据 VO
export interface DeviceHistoryDataVO {
time: number // 时间
data: string // 数据
}
// IoT 设备状态枚举
export enum DeviceStateEnum {
INACTIVE = 0, // 未激活
ONLINE = 1, // 在线
OFFLINE = 2 // 离线
}
// IoT 设备上行 Request VO
export interface IotDeviceUpstreamReqVO {
id: number // 设备编号
type: string // 消息类型
identifier: string // 标识符
data: any // 请求参数
}
// IoT 设备下行 Request VO
export interface IotDeviceDownstreamReqVO {
id: number // 设备编号
type: string // 消息类型
identifier: string // 标识符
data: any // 请求参数
}
// MQTT 连接参数 VO
export interface MqttConnectionParamsVO {
mqttClientId: string // MQTT 客户端 ID
mqttUsername: string // MQTT 用户名
mqttPassword: string // MQTT 密码
}
// 设备 API
export const DeviceApi = {
// 查询设备分页
getDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/page`, params })
},
// 查询设备详情
getDevice: async (id: number) => {
return await request.get({ url: `/iot/device/get?id=` + id })
},
// 新增设备
createDevice: async (data: DeviceVO) => {
return await request.post({ url: `/iot/device/create`, data })
},
// 修改设备
updateDevice: async (data: DeviceVO) => {
return await request.put({ url: `/iot/device/update`, data })
},
// 修改设备分组
updateDeviceGroup: async (data: { ids: number[]; groupIds: number[] }) => {
return await request.put({ url: `/iot/device/update-group`, data })
},
// 删除单个设备
deleteDevice: async (id: number) => {
return await request.delete({ url: `/iot/device/delete?id=` + id })
},
// 删除多个设备
deleteDeviceList: async (ids: number[]) => {
return await request.delete({ url: `/iot/device/delete-list`, params: { ids: ids.join(',') } })
},
// 导出设备
exportDeviceExcel: async (params: any) => {
return await request.download({ url: `/iot/device/export-excel`, params })
},
// 获取设备数量
getDeviceCount: async (productId: number) => {
return await request.get({ url: `/iot/device/count?productId=` + productId })
},
// 获取设备的精简信息列表
getSimpleDeviceList: async (deviceType?: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } })
},
// 获取导入模板
importDeviceTemplate: async () => {
return await request.download({ url: `/iot/device/get-import-template` })
},
// 设备上行
upstreamDevice: async (data: IotDeviceUpstreamReqVO) => {
return await request.post({ url: `/iot/device/upstream`, data })
},
// 设备下行
downstreamDevice: async (data: IotDeviceDownstreamReqVO) => {
return await request.post({ url: `/iot/device/downstream`, data })
},
// 获取设备属性最新数据
getLatestDeviceProperties: async (params: any) => {
return await request.get({ url: `/iot/device/property/latest`, params })
},
// 获取设备属性历史数据
getHistoryDevicePropertyPage: async (params: any) => {
return await request.get({ url: `/iot/device/property/history-page`, params })
},
// 查询设备日志分页
getDeviceLogPage: async (params: any) => {
return await request.get({ url: `/iot/device/log/page`, params })
},
// 获取设备MQTT连接参数
getMqttConnectionParams: async (deviceId: number) => {
return await request.get({ url: `/iot/device/mqtt-connection-params`, params: { deviceId } })
}
}
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' })
}
}
import request from '@/config/axios'
// IoT 产品 VO
export interface ProductVO {
id: number // 产品编号
name: string // 产品名称
productKey: string // 产品标识
protocolId: number // 协议编号
categoryId: number // 产品所属品类标识符
categoryName?: string // 产品所属品类名称
icon: string // 产品图标
picUrl: string // 产品图片
description: string // 产品描述
validateType: number // 数据校验级别
status: number // 产品状态
deviceType: number // 设备类型
netType: number // 联网方式
protocolType: number // 接入网关协议
dataFormat: number // 数据格式
deviceCount: number // 设备数量
createTime: Date // 创建时间
}
// IOT 数据校验级别枚举类
export enum ValidateTypeEnum {
WEAK = 0, // 弱校验
NONE = 1 // 免校验
}
// IOT 产品设备类型枚举类 0: 直连设备, 1: 网关子设备, 2: 网关设备
export enum DeviceTypeEnum {
DEVICE = 0, // 直连设备
GATEWAY_SUB = 1, // 网关子设备
GATEWAY = 2 // 网关设备
}
// IOT 数据格式枚举类
export enum DataFormatEnum {
JSON = 0, // 标准数据格式(JSON)
CUSTOMIZE = 1 // 透传/自定义
}
// IoT 产品 API
export const ProductApi = {
// 查询产品分页
getProductPage: async (params: any) => {
return await request.get({ url: `/iot/product/page`, params })
},
// 查询产品详情
getProduct: async (id: number) => {
return await request.get({ url: `/iot/product/get?id=` + id })
},
// 新增产品
createProduct: async (data: ProductVO) => {
return await request.post({ url: `/iot/product/create`, data })
},
// 修改产品
updateProduct: async (data: ProductVO) => {
return await request.put({ url: `/iot/product/update`, data })
},
// 删除产品
deleteProduct: async (id: number) => {
return await request.delete({ url: `/iot/product/delete?id=` + id })
},
// 导出产品 Excel
exportProduct: async (params) => {
return await request.download({ url: `/iot/product/export-excel`, params })
},
// 更新产品状态
updateProductStatus: async (id: number, status: number) => {
return await request.put({ url: `/iot/product/update-status?id=` + id + `&status=` + status })
},
// 查询产品(精简)列表
getSimpleProductList() {
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 })
}
}
......@@ -381,49 +381,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
breadcrumb: false
}
},
{
path: '/iot',
component: Layout,
name: 'IOT',
meta: {
hidden: true
},
children: [
{
path: 'product/product/detail/:id',
name: 'IoTProductDetail',
meta: {
title: '产品详情',
noCache: true,
hidden: true,
activeMenu: '/iot/device/product'
},
component: () => import('@/views/iot/product/product/detail/index.vue')
},
{
path: 'device/detail/:id',
name: 'IoTDeviceDetail',
meta: {
title: '设备详情',
noCache: true,
hidden: true,
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/plugin/detail/index.vue')
}
]
}
]
......
<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">
<div>
<el-col>
<el-row>
<span class="text-xl font-bold">{{ device.deviceName }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上:按钮 -->
<el-button
@click="openForm('update', device.id)"
v-hasPermi="['iot:device:update']"
v-if="product.status === 0"
>
编辑
</el-button>
</div>
</div>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="产品">
<el-link @click="goToProductDetail(product.id)">{{ product.name }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<DeviceForm ref="formRef" @success="emit('refresh')" />
</template>
<script setup lang="ts">
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 copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
message.success('复制成功')
} catch (error) {
message.error('复制失败')
}
}
/** 跳转到产品详情页面 */
const goToProductDetail = (productId: number) => {
router.push({ name: 'IoTProductDetail', params: { id: productId } })
}
</script>
<!-- 设备信息 -->
<template>
<ContentWrap>
<el-descriptions :column="3" title="设备信息">
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
</el-descriptions-item>
<el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item>
<el-descriptions-item label="DeviceName">
{{ device.deviceName }}
<el-button @click="copyToClipboard(device.deviceName)">复制</el-button>
</el-descriptions-item>
<el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(device.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="当前状态">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
</el-descriptions-item>
<el-descriptions-item label="激活时间">
{{ formatDate(device.activeTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后上线时间">
{{ formatDate(device.onlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后离线时间" :span="3">
{{ formatDate(device.offlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="MQTT 连接参数">
<el-button type="primary" @click="openMqttParams">查看</el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- MQTT 连接参数弹框 -->
<Dialog
title="MQTT 连接参数"
v-model="mqttDialogVisible"
width="50%"
:before-close="handleCloseMqttDialog"
>
<el-form :model="mqttParams" label-width="120px">
<el-form-item label="clientId">
<el-input v-model="mqttParams.mqttClientId" readonly>
<template #append>
<el-button @click="copyToClipboard(mqttParams.mqttClientId)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="username">
<el-input v-model="mqttParams.mqttUsername" readonly>
<template #append>
<el-button @click="copyToClipboard(mqttParams.mqttUsername)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="passwd">
<el-input
v-model="mqttParams.mqttPassword"
readonly
:type="passwordVisible ? 'text' : 'password'"
>
<template #append>
<el-button @click="passwordVisible = !passwordVisible" type="primary">
<Icon :icon="passwordVisible ? 'ph:eye-slash' : 'ph:eye'" />
</el-button>
<el-button @click="copyToClipboard(mqttParams.mqttPassword)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="mqttDialogVisible = false">关闭</el-button>
</template>
</Dialog>
<!-- TODO 待开发:设备标签 -->
<!-- TODO 待开发:设备地图 -->
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { ProductVO } from '@/api/iot/product/product'
import { formatDate } from '@/utils/formatTime'
import { DeviceVO } from '@/api/iot/device/device'
import { DeviceApi, MqttConnectionParamsVO } from '@/api/iot/device/device/index'
const message = useMessage() // 消息提示
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
const emit = defineEmits(['refresh']) // 定义 Emits
const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性
const passwordVisible = ref(false) // 定义密码可见性状态
const mqttParams = ref({
mqttClientId: '',
mqttUsername: '',
mqttPassword: ''
}) // 定义 MQTT 参数对象
/** 复制到剪贴板方法 */
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
message.success('复制成功')
} catch (error) {
message.error('复制失败')
}
}
/** 打开 MQTT 参数弹框的方法 */
const openMqttParams = async () => {
try {
const data = await DeviceApi.getMqttConnectionParams(device.id)
// 根据 API 响应结构正确获取数据
// TODO @haohao:'N/A' 是不是在 ui 里处理哈
mqttParams.value = {
mqttClientId: data.mqttClientId || 'N/A',
mqttUsername: data.mqttUsername || 'N/A',
mqttPassword: data.mqttPassword || 'N/A'
}
// 显示 MQTT 弹框
mqttDialogVisible.value = true
} catch (error) {
console.error('获取 MQTT 连接参数出错:', error)
message.error('获取MQTT连接参数失败,请检查网络连接或联系管理员')
}
}
/** 关闭 MQTT 弹框的方法 */
const handleCloseMqttDialog = () => {
mqttDialogVisible.value = false
}
</script>
<!-- 设备日志 -->
<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>
<template>
<DeviceDetailsHeader
:loading="loading"
:product="product"
:device="device"
@refresh="getDeviceData(id)"
/>
<el-col>
<el-tabs v-model="activeTab">
<el-tab-pane label="设备信息" name="info">
<DeviceDetailsInfo v-if="activeTab === 'info'" :product="product" :device="device" />
</el-tab-pane>
<el-tab-pane label="Topic 列表" />
<el-tab-pane label="物模型数据" name="model">
<DeviceDetailsModel v-if="activeTab === 'model'" :product="product" :device="device" />
</el-tab-pane>
<el-tab-pane label="子设备管理" v-if="product.deviceType === DeviceTypeEnum.GATEWAY" />
<el-tab-pane label="设备影子" />
<el-tab-pane label="设备日志" name="log">
<DeviceDetailsLog v-if="activeTab === 'log'" :device-key="device.deviceKey" />
</el-tab-pane>
<el-tab-pane label="模拟设备" name="simulator">
<DeviceDetailsSimulator
v-if="activeTab === 'simulator'"
:product="product"
:device="device"
/>
</el-tab-pane>
<el-tab-pane label="设备配置" name="config">
<DeviceDetailConfig
v-if="activeTab === 'config'"
:device="device"
@success="getDeviceData"
/>
</el-tab-pane>
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
import DeviceDetailsModel from './DeviceDetailsModel.vue'
import DeviceDetailsLog from './DeviceDetailsLog.vue'
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
import DeviceDetailConfig from './DeviceDetailConfig.vue'
defineOptions({ name: 'IoTDeviceDetail' })
const route = useRoute()
const message = useMessage()
const id = route.params.id // 将字符串转换为数字
const loading = ref(true) // 加载中
const product = ref<ProductVO>({} as ProductVO) // 产品详情
const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
const activeTab = ref('info') // 默认激活的标签页
/** 获取设备详情 */
const getDeviceData = async () => {
loading.value = true
try {
device.value = await DeviceApi.getDevice(id)
await getProductData(device.value.productId)
} finally {
loading.value = false
}
}
/** 获取产品详情 */
const getProductData = async (id: number) => {
product.value = await ProductApi.getProduct(id)
}
/** 初始化 */
const { delView } = useTagsViewStore() // 视图操作
const { currentRoute } = useRouter() // 路由
onMounted(async () => {
if (!id) {
message.warning('参数错误,产品不能为空!')
delView(unref(currentRoute))
return
}
await getDeviceData()
activeTab.value = (route.query.tab as string) || 'info'
})
</script>
<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>
<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:device-group: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="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" prop="deviceCount" />
<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:device-group:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:device-group: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>
<!-- 表单弹窗:添加/修改 -->
<DeviceGroupForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
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: 'IoTDeviceGroup' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<DeviceGroupVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceGroupApi.getDeviceGroupPage(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 DeviceGroupApi.deleteDeviceGroup(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="name">
<el-input v-model="formData.name" placeholder="请输入插件名称" />
</el-form-item>
<el-form-item label="部署方式" prop="deployType">
<el-select v-model="formData.deployType" placeholder="请选择部署方式">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { PluginConfigApi, PluginConfigVO } from '@/api/iot/plugin'
/** IoT 插件配置 表单 */
defineOptions({ name: 'PluginConfigForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: undefined,
deployType: undefined
})
const formRules = reactive({
name: [{ required: true, message: '插件名称不能为空', trigger: 'blur' }],
deployType: [{ required: true, message: '部署方式不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await PluginConfigApi.getPluginConfig(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as PluginConfigVO
if (formType.value === 'create') {
await PluginConfigApi.createPluginConfig(data)
message.success(t('common.createSuccess'))
} else {
await PluginConfigApi.updatePluginConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
deployType: undefined
}
formRef.value?.resetFields()
}
</script>
<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="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="categoryId">
<el-select v-model="formData.categoryId" placeholder="请选择产品分类" clearable>
<el-option
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="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(formData.deviceType)"
label="联网方式"
prop="netType"
>
<el-select
v-model="formData.netType"
placeholder="请选择联网方式"
:disabled="formType === 'update'"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_NET_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.deviceType === DeviceTypeEnum.GATEWAY_SUB"
label="接入网关协议"
prop="protocolType"
>
<el-select
v-model="formData.protocolType"
placeholder="请选择接入网关协议"
:disabled="formType === 'update'"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="数据格式" prop="dataFormat">
<el-radio-group v-model="formData.dataFormat" :disabled="formType === 'update'">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_FORMAT)"
:key="dict.value"
: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
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</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>
</template>
</Dialog>
</template>
<script setup lang="ts">
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' })
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,
name: undefined,
productKey: '',
categoryId: undefined,
icon: undefined,
picUrl: undefined,
description: undefined,
deviceType: undefined,
netType: undefined,
protocolType: 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: [
{
required: true,
message: '联网方式不能为空',
trigger: 'change'
}
],
protocolType: [
{
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) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
formData.value = await ProductApi.getProduct(id)
} finally {
formLoading.value = false
}
} else {
// 新增时,生成随机 productKey
generateProductKey()
}
// 加载分类列表
categoryList.value = await ProductCategoryApi.getSimpleProductCategoryList()
}
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 ProductVO
if (formType.value === 'create') {
await ProductApi.createProduct(data)
message.success(t('common.createSuccess'))
} else {
await ProductApi.updateProduct(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false // 确保关闭弹框
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
productKey: '',
categoryId: undefined,
icon: undefined,
picUrl: undefined,
description: undefined,
deviceType: undefined,
netType: undefined,
protocolType: undefined,
protocolId: undefined,
dataFormat: DataFormatEnum.JSON,
validateType: ValidateTypeEnum.WEAK
}
formRef.value?.resetFields()
}
/** 生成 ProductKey */
const generateProductKey = () => {
formData.value.productKey = generateRandomStr(16)
}
</script>
<template>
<div>
<div class="flex items-start justify-between">
<div>
<el-col>
<el-row>
<span class="text-xl font-bold">{{ product.name }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上:按钮 -->
<el-button
@click="openForm('update', product.id)"
v-hasPermi="['iot:product:update']"
v-if="product.status === 0"
>
编辑
</el-button>
<el-button
type="primary"
@click="confirmPublish(product.id)"
v-hasPermi="['iot:product:update']"
v-if="product.status === 0"
>
发布
</el-button>
<el-button
type="danger"
@click="confirmUnpublish(product.id)"
v-hasPermi="['iot:product:update']"
v-if="product.status === 1"
>
撤销发布
</el-button>
</div>
</div>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="设备数">
{{ product.deviceCount ?? '加载中...' }}
<el-button @click="goToDeviceList(product.id)">前往管理</el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<ProductForm ref="formRef" @success="emit('refresh')" />
</template>
<script setup lang="ts">
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 = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
message.success('复制成功')
} catch (error) {
message.error('复制失败')
}
}
/** 路由跳转到设备管理 */
const { push } = useRouter()
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)
message.success('发布成功')
formRef.value.close() // 关闭弹框
emit('refresh')
} catch (error) {
message.error('发布失败')
}
}
/** 撤销发布操作 */
const confirmUnpublish = async (id: number) => {
try {
await ProductApi.updateProductStatus(id, 0)
message.success('撤销发布成功')
formRef.value.close() // 关闭弹框
emit('refresh')
} catch (error) {
message.error('撤销发布失败')
}
}
</script>
<template>
<ContentWrap>
<el-descriptions :column="3" title="产品信息">
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
<el-descriptions-item label="所属分类">{{ product.categoryName }}</el-descriptions-item>
<el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(product.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="数据格式">
<dict-tag :type="DICT_TYPE.IOT_DATA_FORMAT" :value="product.dataFormat" />
</el-descriptions-item>
<el-descriptions-item label="数据校验级别">
<dict-tag :type="DICT_TYPE.IOT_VALIDATE_TYPE" :value="product.validateType" />
</el-descriptions-item>
<el-descriptions-item label="产品状态">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</el-descriptions-item>
<el-descriptions-item
label="联网方式"
v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(product.deviceType)"
>
<dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</el-descriptions-item>
<el-descriptions-item
label="接入网关协议"
v-if="product.deviceType === DeviceTypeEnum.GATEWAY_SUB"
>
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL_TYPE" :value="product.protocolType" />
</el-descriptions-item>
<el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
</el-descriptions>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { DeviceTypeEnum, ProductVO } from '@/api/iot/product/product'
import { formatDate } from '@/utils/formatTime'
const { product } = defineProps<{ product: ProductVO }>()
</script>
<template>
<ContentWrap>
<el-tabs>
<el-tab-pane label="基础通信 Topic">
<Table
:columns="basicColumn"
:data="basicData"
:span-method="createSpanMethod(basicData)"
align="left"
headerAlign="left"
border="true"
/>
</el-tab-pane>
<el-tab-pane label="物模型通信 Topic">
<Table
:columns="functionColumn"
:data="functionData"
:span-method="createSpanMethod(functionData)"
align="left"
headerAlign="left"
border="true"
/>
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
import { ProductVO } from '@/api/iot/product/product'
const props = defineProps<{ product: ProductVO }>()
// TODO 芋艿:不确定未来会不会改,所以先写死
// 基础通信 Topic 列
const basicColumn = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
// 基础通信 Topic 数据
const basicData = computed(() => {
if (!props.product || !props.product.productKey) return []
return [
{
function: 'OTA 升级',
topicClass: `/ota/device/inform/${props.product.productKey}/\${deviceName}`,
operationPermission: '发布',
description: '设备上报固件升级信息'
},
{
function: 'OTA 升级',
topicClass: `/ota/device/upgrade/${props.product.productKey}/\${deviceName}`,
operationPermission: '订阅',
description: '固件升级信息下行'
},
{
function: 'OTA 升级',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
operationPermission: '发布',
description: '设备上报固件升级进度'
},
{
function: 'OTA 升级',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
operationPermission: '发布',
description: '设备主动拉取固件升级信息'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update`,
operationPermission: '发布',
description: '设备上报标签数据'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update_reply`,
operationPermission: '订阅',
description: '云端响应标签上报'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete`,
operationPermission: '订阅',
description: '设备删除标签信息'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete_reply`,
operationPermission: '订阅',
description: '云端响应标签删除'
},
{
function: '时钟同步',
topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/request`,
operationPermission: '发布',
description: 'NTP 时钟同步请求'
},
{
function: '时钟同步',
topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/response`,
operationPermission: '订阅',
description: 'NTP 时钟同步响应'
},
{
function: '设备影子',
topicClass: `/shadow/update/${props.product.productKey}/\${deviceName}`,
operationPermission: '发布',
description: '设备影子发布'
},
{
function: '设备影子',
topicClass: `/shadow/get/${props.product.productKey}/\${deviceName}`,
operationPermission: '订阅',
description: '设备接收影子变更'
},
{
function: '配置更新',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/push`,
operationPermission: '订阅',
description: '云端主动下推配置信息'
},
{
function: '配置更新',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get`,
operationPermission: '发布',
description: '设备端查询配置信息'
},
{
function: '配置更新',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get_reply`,
operationPermission: '订阅',
description: '云端响应配置信息'
},
{
function: '广播',
topicClass: `/broadcast/${props.product.productKey}/\${identifier}`,
operationPermission: '订阅',
description: '广播 Topic,identifier 为用户自定义字符串'
}
]
})
// 物模型通信 Topic 列
const functionColumn = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
// 物模型通信 Topic 数据
const functionData = computed(() => {
if (!props.product || !props.product.productKey) return []
return [
{
function: '属性上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post`,
operationPermission: '发布',
description: '设备属性上报'
},
{
function: '属性上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post_reply`,
operationPermission: '订阅',
description: '云端响应属性上报'
},
{
function: '属性设置',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/property/set`,
operationPermission: '订阅',
description: '设备属性设置'
},
{
function: '事件上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post`,
operationPermission: '发布',
description: '设备事件上报'
},
{
function: '事件上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post_reply`,
operationPermission: '订阅',
description: '云端响应事件上报'
},
{
function: '服务调用',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}`,
operationPermission: '订阅',
description: '设备服务调用'
},
{
function: '服务调用',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}_reply`,
operationPermission: '发布',
description: '设备端响应服务调用'
}
]
})
// 通用的单元格合并方法生成器
const createSpanMethod = (data: any[]) => {
// 预处理,计算每个功能的合并行数
const rowspanMap: Record<number, number> = {}
let currentFunction = ''
let startIndex = 0
let count = 0
data.forEach((item, index) => {
if (item.function !== currentFunction) {
if (count > 0) {
rowspanMap[startIndex] = count
}
currentFunction = item.function
startIndex = index
count = 1
} else {
count++
}
})
// 处理最后一组
if (count > 0) {
rowspanMap[startIndex] = count
}
// 返回 span 方法
return ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
if (columnIndex === 0) {
// 仅对“功能”列进行合并
const rowspan = rowspanMap[rowIndex] || 0
if (rowspan > 0) {
return {
rowspan,
colspan: 1
}
} else {
return {
rowspan: 0,
colspan: 0
}
}
}
}
}
</script>
<template>
<ProductDetailsHeader :loading="loading" :product="product" @refresh="() => getProductData(id)" />
<el-col>
<el-tabs v-model="activeTab">
<el-tab-pane label="产品信息" name="info">
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
</el-tab-pane>
<el-tab-pane label="Topic 类列表" name="topic">
<ProductTopic v-if="activeTab === 'topic'" :product="product" />
</el-tab-pane>
<el-tab-pane label="功能定义" lazy name="thingModel">
<IoTProductThingModel ref="thingModelRef" />
</el-tab-pane>
<el-tab-pane label="消息解析" name="message" />
<el-tab-pane label="服务端订阅" name="subscription" />
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
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' })
const { delView } = useTagsViewStore() // 视图操作
const { currentRoute } = useRouter()
const route = useRoute()
const message = useMessage()
const id = route.params.id // 编号
const loading = ref(true) // 加载中
const product = ref<ProductVO>({} as ProductVO) // 详情
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)
} finally {
loading.value = false
}
}
/** 查询设备数量 */
const getDeviceCount = async (productId: number) => {
try {
return await DeviceApi.getDeviceCount(productId)
} catch (error) {
console.error('Error fetching device count:', error, 'productId:', productId)
return 0
}
}
/** 初始化 */
onMounted(async () => {
if (!id) {
message.warning('参数错误,产品不能为空!')
delView(unref(currentRoute))
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)
}
})
</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
}
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="桥梁名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入桥梁名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="桥梁状态" prop="status">
<el-select
v-model="queryParams.status"
class="!w-240px"
clearable
placeholder="请选择桥梁状态"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="桥梁方向" prop="direction">
<el-select
v-model="queryParams.direction"
class="!w-240px"
clearable
placeholder="请选择桥梁方向"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_DIRECTION_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="桥梁类型" prop="type">
<el-select
v-model="queryParams.type"
class="!w-240px"
clearable
placeholder="请选择桥梁类型"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button
v-hasPermi="['iot:data-bridge:create']"
plain
type="primary"
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="桥梁编号" prop="id" />
<el-table-column align="center" label="桥梁名称" prop="name" />
<el-table-column align="center" label="桥梁描述" prop="description" />
<el-table-column align="center" label="桥梁状态" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="桥梁方向" prop="direction">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DATA_BRIDGE_DIRECTION_ENUM" :value="scope.row.direction" />
</template>
</el-table-column>
<el-table-column align="center" label="桥梁类型" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column align="center" fixed="right" label="操作" width="120px">
<template #default="scope">
<el-button
v-hasPermi="['iot:data-bridge:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['iot:data-bridge:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<DataBridgeForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { DataBridgeApi, DataBridgeVO } from '@/api/iot/rule/databridge'
import DataBridgeForm from './IoTDataBridgeForm.vue'
/** IoT 数据桥梁 列表 */
defineOptions({ name: 'IotDataBridge' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<DataBridgeVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
description: undefined,
status: undefined,
direction: undefined,
type: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DataBridgeApi.getDataBridgePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DataBridgeApi.deleteDataBridge(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<!-- 产品的物模型表单(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 () => {
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>
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 to comment