【新增】 IOT 产品管理,Topic 类列表
parent
93a0789e34
commit
6aaf611291
|
@ -67,8 +67,8 @@ export const DeviceApi = {
|
|||
return await request.delete({ url: `/iot/device/delete?id=` + id })
|
||||
},
|
||||
|
||||
// 导出设备 Excel
|
||||
exportDevice: async (params) => {
|
||||
return await request.download({ url: `/iot/device/export-excel`, params })
|
||||
// 获取设备数量
|
||||
getDeviceCount: async (productId: number) => {
|
||||
return await request.get({ url: `/iot/device/count?productId=` + productId })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export interface ProductVO {
|
|||
netType: number // 联网方式, 0: Wi-Fi, 1: Cellular, 2: Ethernet, 3: 其他
|
||||
protocolType: number // 接入网关协议, 0: modbus, 1: opc-ua, 2: customize, 3: ble, 4: zigbee
|
||||
dataFormat: number // 数据格式, 0: 透传模式, 1: Alink JSON
|
||||
deviceCount: number // 设备数量
|
||||
}
|
||||
|
||||
// iot 产品 API
|
||||
|
|
|
@ -58,49 +58,10 @@ const emit = defineEmits(['refresh'])
|
|||
*
|
||||
* @param text 需要复制的文本
|
||||
*/
|
||||
const copyToClipboard = async (text: string) => {
|
||||
if (!navigator.clipboard) {
|
||||
// 浏览器不支持 Clipboard API,使用回退方法
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
// 防止页面滚动
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.top = '0'
|
||||
textarea.style.left = '0'
|
||||
textarea.style.width = '2em'
|
||||
textarea.style.height = '2em'
|
||||
textarea.style.padding = '0'
|
||||
textarea.style.border = 'none'
|
||||
textarea.style.outline = 'none'
|
||||
textarea.style.boxShadow = 'none'
|
||||
textarea.style.background = 'transparent'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy')
|
||||
if (successful) {
|
||||
message.success('复制成功!')
|
||||
} else {
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
message.success('复制成功!')
|
||||
} catch (err) {
|
||||
console.error('Async: Could not copy text: ', err)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success('复制成功')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,49 +103,10 @@ const emit = defineEmits(['refresh'])
|
|||
const activeNames = ref(['basicInfo'])
|
||||
|
||||
// 复制到剪贴板方法
|
||||
const copyToClipboard = async (text: string) => {
|
||||
if (!navigator.clipboard) {
|
||||
// 浏览器不支持 Clipboard API,使用回退方法
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
// 防止页面滚动
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.top = '0'
|
||||
textarea.style.left = '0'
|
||||
textarea.style.width = '2em'
|
||||
textarea.style.height = '2em'
|
||||
textarea.style.padding = '0'
|
||||
textarea.style.border = 'none'
|
||||
textarea.style.outline = 'none'
|
||||
textarea.style.boxShadow = 'none'
|
||||
textarea.style.background = 'transparent'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy')
|
||||
if (successful) {
|
||||
message.success('复制成功!')
|
||||
} else {
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
message.success('复制成功!')
|
||||
} catch (err) {
|
||||
console.error('Async: Could not copy text: ', err)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success('复制成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 定义 MQTT 弹框的可见性
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
</el-descriptions>
|
||||
<el-descriptions :column="5" direction="horizontal">
|
||||
<el-descriptions-item label="设备数">
|
||||
0
|
||||
<el-button @click="goToManagement(product.productKey)">前往管理</el-button>
|
||||
{{ product.deviceCount }}
|
||||
<el-button @click="goToManagement(product.id)">前往管理</el-button>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</ContentWrap>
|
||||
|
@ -63,8 +63,11 @@ const copyToClipboard = (text: string) => {
|
|||
message.success('复制成功')
|
||||
})
|
||||
}
|
||||
const goToManagement = (productKey: string) => {
|
||||
message.warning('暂未开放')
|
||||
|
||||
// 路由跳转到设备管理
|
||||
const { currentRoute, push } = useRouter()
|
||||
const goToManagement = (productId: string) => {
|
||||
push({ name: 'IoTDevice', query: { productId } })
|
||||
}
|
||||
|
||||
// 操作修改
|
||||
|
@ -93,6 +96,9 @@ const confirmUnpublish = async (id: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 定义 Props
|
||||
const { product } = defineProps<{ product: ProductVO }>()
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits(['refresh'])
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="基础通信 Topic">
|
||||
<Table :columns="columns1" :data="data1" :span-method="objectSpanMethod" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="物模型通信 Topic">
|
||||
<Table :columns="columns2" :data="data2" :span-method="objectSpanMethod" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ProductVO } from '@/api/iot/product'
|
||||
|
||||
const { product } = defineProps<{ product: ProductVO }>()
|
||||
// 定义列
|
||||
const columns1 = reactive([
|
||||
{
|
||||
label: '功能',
|
||||
field: 'function',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
label: 'Topic 类',
|
||||
field: 'topicClass',
|
||||
width: 500
|
||||
},
|
||||
{
|
||||
label: '操作权限',
|
||||
field: 'operationPermission',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
field: 'description'
|
||||
}
|
||||
])
|
||||
|
||||
const columns2 = reactive([
|
||||
{
|
||||
label: '功能',
|
||||
field: 'function',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
label: 'Topic 类',
|
||||
field: 'topicClass',
|
||||
width: 500
|
||||
},
|
||||
{
|
||||
label: '操作权限',
|
||||
field: 'operationPermission',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
field: 'description'
|
||||
}
|
||||
])
|
||||
|
||||
// 定义数据
|
||||
const data1 = reactive([
|
||||
{
|
||||
function: 'OTA 升级',
|
||||
topicClass: '/ota/device/inform/' + product.productKey + '/${deviceName}',
|
||||
operationPermission: '发布',
|
||||
description: '设备上报固件升级信息'
|
||||
},
|
||||
{
|
||||
function: 'OTA 升级',
|
||||
topicClass: '/ota/device/upgrade/' + product.productKey + '/${deviceName}',
|
||||
operationPermission: '订阅',
|
||||
description: '固件升级信息下行'
|
||||
},
|
||||
{
|
||||
function: 'OTA 升级',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/ota/firmware/get',
|
||||
operationPermission: '发布',
|
||||
description: '设备上报固件升级进度'
|
||||
},
|
||||
{
|
||||
function: 'OTA 升级',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/ota/firmware/get',
|
||||
operationPermission: '发布',
|
||||
description: '设备主动拉取固件升级信息'
|
||||
},
|
||||
{
|
||||
function: '设备标签',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/deviceinfo/update',
|
||||
operationPermission: '发布',
|
||||
description: '设备上报标签数据'
|
||||
},
|
||||
{
|
||||
function: '设备标签',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/deviceinfo/update_reply',
|
||||
operationPermission: '订阅',
|
||||
description: '云端响应标签上报'
|
||||
},
|
||||
{
|
||||
function: '设备标签',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/deviceinfo/delete',
|
||||
operationPermission: '订阅',
|
||||
description: '设备删除标签信息'
|
||||
},
|
||||
{
|
||||
function: '设备标签',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/deviceinfo/delete_reply',
|
||||
operationPermission: '订阅',
|
||||
description: '云端响应标签删除'
|
||||
},
|
||||
{
|
||||
function: '时钟同步',
|
||||
topicClass: '/ext/ntp/' + product.productKey + '/${deviceName}/request',
|
||||
operationPermission: '发布',
|
||||
description: 'NTP 时钟同步请求'
|
||||
},
|
||||
{
|
||||
function: '时钟同步',
|
||||
topicClass: '/ext/ntp/' + product.productKey + '/${deviceName}/response',
|
||||
operationPermission: '订阅',
|
||||
description: 'NTP 时钟同步响应'
|
||||
},
|
||||
{
|
||||
function: '设备影子',
|
||||
topicClass: '/shadow/update/' + product.productKey + '/${deviceName}',
|
||||
operationPermission: '发布',
|
||||
description: '设备影子发布'
|
||||
},
|
||||
{
|
||||
function: '设备影子',
|
||||
topicClass: '/shadow/get/' + product.productKey + '/${deviceName}',
|
||||
operationPermission: '订阅',
|
||||
description: '设备接收影子变更'
|
||||
},
|
||||
{
|
||||
function: '配置更新',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/config/push',
|
||||
operationPermission: '订阅',
|
||||
description: '云端主动下推配置信息'
|
||||
},
|
||||
{
|
||||
function: '配置更新',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/config/get',
|
||||
operationPermission: '发布',
|
||||
description: '设备端查询配置信息'
|
||||
},
|
||||
{
|
||||
function: '配置更新',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/config/get_reply',
|
||||
operationPermission: '订阅',
|
||||
description: '云端响应配置信息'
|
||||
},
|
||||
{
|
||||
function: '广播',
|
||||
topicClass: '/broadcast/' + product.productKey + '/${identifier}',
|
||||
operationPermission: '订阅',
|
||||
description: '广播 Topic,identifier 为用户自定义字符串'
|
||||
}
|
||||
])
|
||||
|
||||
const data2 = reactive([
|
||||
{
|
||||
function: '属性上报',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/event/property/post',
|
||||
operationPermission: '发布',
|
||||
description: '设备属性上报'
|
||||
},
|
||||
{
|
||||
function: '属性上报',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/event/property/post_reply',
|
||||
operationPermission: '订阅',
|
||||
description: '云端响应属性上报'
|
||||
},
|
||||
{
|
||||
function: '属性设置',
|
||||
topicClass: '/sys/' + product.productKey + '/${deviceName}/thing/service/property/set',
|
||||
operationPermission: '订阅',
|
||||
description: '设备属性设置'
|
||||
},
|
||||
{
|
||||
function: '事件上报',
|
||||
topicClass:
|
||||
'/sys/' + product.productKey + '/${deviceName}/thing/event/${tsl.event.identifier}/post',
|
||||
operationPermission: '发布',
|
||||
description: '设备事件上报'
|
||||
},
|
||||
{
|
||||
function: '事件上报',
|
||||
topicClass:
|
||||
'/sys/' +
|
||||
product.productKey +
|
||||
'/${deviceName}/thing/event/${tsl.event.identifier}/post_reply',
|
||||
operationPermission: '订阅',
|
||||
description: '云端响应事件上报'
|
||||
},
|
||||
{
|
||||
function: '服务调用',
|
||||
topicClass:
|
||||
'/sys/' + product.productKey + '/${deviceName}/thing/service/${tsl.service.identifier}',
|
||||
operationPermission: '订阅',
|
||||
description: '设备服务调用'
|
||||
},
|
||||
{
|
||||
function: '服务调用',
|
||||
topicClass:
|
||||
'/sys/' + product.productKey + '/${deviceName}/thing/service/${tsl.service.identifier}_reply',
|
||||
operationPermission: '发布',
|
||||
description: '设备端响应服务调用'
|
||||
}
|
||||
])
|
||||
|
||||
// 合并单元格
|
||||
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
|
||||
if (columnIndex === 0 || columnIndex === 2) {
|
||||
if (rowIndex % 2 === 0) {
|
||||
return {
|
||||
rowspan: 2,
|
||||
colspan: 1
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rowspan: 0,
|
||||
colspan: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<ProductDetailsHeader :loading="loading" :product="product" @refresh="getProductData(id)" />
|
||||
<ProductDetailsHeader :loading="loading" :product="product" @refresh="() => getProductData(id)" />
|
||||
<el-col>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="产品信息">
|
||||
<ProductDetailsInfo :product="product" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Topic 类列表" />
|
||||
<el-tab-pane label="Topic 类列表">
|
||||
<ProductTopic :product="product" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="物模型">
|
||||
<!-- <ProductDetailsModel :product="product" />-->
|
||||
</el-tab-pane>
|
||||
|
@ -15,10 +17,10 @@
|
|||
</el-col>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { ProductApi, ProductVO } from '@/api/iot/product'
|
||||
import ProductDetailsHeader from '@/views/iot/product/detail/ProductDetailsHeader.vue'
|
||||
import ProductDetailsInfo from '@/views/iot/product/detail/ProductDetailsInfo.vue'
|
||||
import { DeviceApi } from '@/api/iot/device'
|
||||
|
||||
defineOptions({ name: 'IoTProductDetail' })
|
||||
|
||||
|
@ -33,17 +35,25 @@ const getProductData = async (id: number) => {
|
|||
loading.value = true
|
||||
try {
|
||||
product.value = await ProductApi.getProduct(id)
|
||||
console.log(product.value)
|
||||
console.log('Product data:', product.value)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取物模型 */
|
||||
// 查询设备数量
|
||||
const getDeviceCount = async (productId: string) => {
|
||||
try {
|
||||
const count = await DeviceApi.getDeviceCount(productId)
|
||||
console.log('Device count response:', count)
|
||||
return count
|
||||
} catch (error) {
|
||||
console.error('Error fetching device count:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
onMounted(async () => {
|
||||
if (!id) {
|
||||
message.warning('参数错误,产品不能为空!')
|
||||
|
@ -51,5 +61,12 @@ onMounted(async () => {
|
|||
return
|
||||
}
|
||||
await getProductData(id)
|
||||
// 查询设备数量
|
||||
if (product.value.id) {
|
||||
product.value.deviceCount = await getDeviceCount(product.value.id)
|
||||
console.log('Device count:', product.value.deviceCount)
|
||||
} else {
|
||||
console.error('Product ID is undefined')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue