Browse Source

【新增】 IOT 产品管理,Topic 类列表

安浩浩 11 months ago
parent
commit
6aaf611291

+ 3 - 3
src/api/iot/device/index.ts

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

+ 1 - 0
src/api/iot/product/index.ts

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

+ 4 - 43
src/views/iot/device/detail/DeviceDetailsHeader.vue

@@ -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('复制成功')
+  })
 }
 
 /**

+ 4 - 43
src/views/iot/device/detail/DeviceDetailsInfo.vue

@@ -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 弹框的可见性

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

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

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

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

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

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