Selaa lähdekoodia

【新增】 IOT 设备管理,设备详情

安浩浩 11 kuukautta sitten
vanhempi
sitoutus
93a0789e34

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

@@ -15,6 +15,7 @@ export interface DeviceVO {
   lastOnlineTime: Date // 最后上线时间
   lastOfflineTime: Date // 最后离线时间
   activeTime: Date // 设备激活时间
+  createTime: Date // 创建时间
   ip: string // 设备的 IP 地址
   firmwareVersion: string // 设备的固件版本
   deviceSecret: string // 设备密钥,用于设备认证,需安全存储

+ 11 - 0
src/router/modules/remaining.ts

@@ -622,6 +622,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/iot/product'
         },
         component: () => import('@/views/iot/product/detail/index.vue')
+      },
+      {
+        path: 'device/detail/:id',
+        name: 'IoTDeviceDetail',
+        meta: {
+          title: '设备详情',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/iot/device'
+        },
+        component: () => import('@/views/iot/device/detail/index.vue')
       }
     ]
   }

+ 114 - 0
src/views/iot/device/detail/DeviceDetailsHeader.vue

@@ -0,0 +1,114 @@
+<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 { ref } from 'vue'
+import DeviceForm from '@/views/iot/device/DeviceForm.vue'
+import { ProductVO } from '@/api/iot/product'
+import { DeviceVO } from '@/api/iot/device'
+import { useRouter } from 'vue-router'
+
+const message = useMessage()
+const router = useRouter()
+
+// 操作修改
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
+const emit = defineEmits(['refresh'])
+
+/**
+ * 将文本复制到剪贴板
+ *
+ * @param text 需要复制的文本
+ */
+const copyToClipboard = 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('复制失败,请手动复制')
+  }
+}
+
+/**
+ * 跳转到产品详情页面
+ *
+ * @param productId 产品 ID
+ */
+const goToProductDetail = (productId: number) => {
+  router.push({ name: 'IoTProductDetail', params: { id: productId } })
+}
+</script>

+ 175 - 0
src/views/iot/device/detail/DeviceDetailsInfo.vue

@@ -0,0 +1,175 @@
+<template>
+  <ContentWrap>
+    <el-collapse v-model="activeNames">
+      <el-descriptions :column="3" title="设备信息">
+        <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
+        <el-descriptions-item label="ProductKey"
+          >{{ product.productKey }}
+          <el-button @click="copyToClipboard(product.productKey)">复制</el-button>
+        </el-descriptions-item>
+        <el-descriptions-item label="设备类型">
+          <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
+        </el-descriptions-item>
+        <el-descriptions-item label="DeviceName"
+          >{{ device.deviceName }}
+          <el-button @click="copyToClipboard(device.deviceName)">复制</el-button>
+        </el-descriptions-item>
+        <el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{
+          formatDate(device.createTime)
+        }}</el-descriptions-item>
+        <el-descriptions-item label="激活时间">{{
+          formatDate(device.activeTime)
+        }}</el-descriptions-item>
+        <el-descriptions-item label="最后上线时间">{{
+          formatDate(device.lastOnlineTime)
+        }}</el-descriptions-item>
+        <el-descriptions-item label="当前状态">
+          <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.status" />
+        </el-descriptions-item>
+        <el-descriptions-item label="最后离线时间" :span="3">{{
+          formatDate(device.lastOfflineTime)
+        }}</el-descriptions-item>
+        <el-descriptions-item label="MQTT 连接参数">
+          <el-button type="primary" @click="openMqttParams">查看</el-button>
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-collapse>
+
+    <!-- MQTT 连接参数弹框 -->
+    <Dialog
+      title="MQTT 连接参数"
+      v-model="mqttDialogVisible"
+      width="50%"
+      :before-close="handleCloseMqttDialog"
+    >
+      <el-form :model="mqttParams" label-width="120px">
+        <el-form-item label="clientId">
+          <el-input v-model="mqttParams.mqttClientId" readonly>
+            <template #append>
+              <el-button @click="copyToClipboard(mqttParams.mqttClientId)" type="primary">
+                <Icon icon="ph:copy" />
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="username">
+          <el-input v-model="mqttParams.mqttUsername" readonly>
+            <template #append>
+              <el-button @click="copyToClipboard(mqttParams.mqttUsername)" type="primary">
+                <Icon icon="ph:copy" />
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="passwd">
+          <el-input v-model="mqttParams.mqttPassword" readonly type="password">
+            <template #append>
+              <el-button @click="copyToClipboard(mqttParams.mqttPassword)" type="primary">
+                <Icon icon="ph:copy" />
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="mqttDialogVisible = false">关闭</el-button>
+      </template>
+    </Dialog>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { ref } from 'vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { ProductVO } from '@/api/iot/product'
+import { formatDate } from '@/utils/formatTime'
+import { DeviceVO } from '@/api/iot/device'
+
+// 消息提示
+const message = useMessage()
+
+// 路由实例
+const router = useRouter()
+
+// 定义 Props
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
+
+// 定义 Emits
+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('复制失败,请手动复制')
+  }
+}
+
+// 定义 MQTT 弹框的可见性
+const mqttDialogVisible = ref(false)
+
+// 定义 MQTT 参数对象
+const mqttParams = ref({
+  mqttClientId: '',
+  mqttUsername: '',
+  mqttPassword: ''
+})
+
+// 打开 MQTT 参数弹框的方法
+const openMqttParams = () => {
+  mqttParams.value = {
+    mqttClientId: device.mqttClientId || 'N/A',
+    mqttUsername: device.mqttUsername || 'N/A',
+    mqttPassword: device.mqttPassword || 'N/A'
+  }
+  mqttDialogVisible.value = true
+}
+
+// 关闭 MQTT 弹框的方法
+const handleCloseMqttDialog = () => {
+  mqttDialogVisible.value = false
+}
+</script>

+ 66 - 0
src/views/iot/device/detail/index.vue

@@ -0,0 +1,66 @@
+<template>
+  <DeviceDetailsHeader
+    :loading="loading"
+    :product="product"
+    :device="device"
+    @refresh="getDeviceData(id)"
+  />
+  <el-col>
+    <el-tabs>
+      <el-tab-pane label="设备信息">
+        <DeviceDetailsInfo :product="product" :device="device" />
+      </el-tab-pane>
+      <el-tab-pane label="Topic 列表" />
+      <el-tab-pane label="物模型数据" />
+      <el-tab-pane label="子设备管理" />
+    </el-tabs>
+  </el-col>
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { DeviceApi, DeviceVO } from '@/api/iot/device'
+import { ProductApi, ProductVO } from '@/api/iot/product'
+import DeviceDetailsHeader from '@/views/iot/device/detail/DeviceDetailsHeader.vue'
+import DeviceDetailsInfo from '@/views/iot/device/detail/DeviceDetailsInfo.vue'
+
+defineOptions({ name: 'IoTDeviceDetail' })
+
+const route = useRoute()
+const message = useMessage()
+const id = Number(route.params.id) // 编号
+const loading = ref(true) // 加载中
+const product = ref<ProductVO>({} as ProductVO) // 产品详情
+const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
+
+/** 获取设备详情 */
+const getDeviceData = async (id: number) => {
+  loading.value = true
+  try {
+    device.value = await DeviceApi.getDevice(id)
+    console.log(product.value)
+    await getProductData(device.value.productId)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取产品详情 */
+const getProductData = async (id: number) => {
+  product.value = await ProductApi.getProduct(id)
+  console.log(product.value)
+}
+
+/** 获取物模型 */
+
+/** 初始化 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
+onMounted(async () => {
+  if (!id) {
+    message.warning('参数错误,产品不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  await getDeviceData(id)
+})
+</script>

+ 20 - 18
src/views/iot/device/index.vue

@@ -96,7 +96,11 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="DeviceName" align="center" prop="deviceName" />
+      <el-table-column label="DeviceName" align="center" prop="deviceName">
+        <template #default="scope">
+          <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
+        </template>
+      </el-table-column>
       <el-table-column label="备注名称" align="center" prop="nickname" />
       <el-table-column label="设备所属产品" align="center" prop="productId">
         <template #default="scope">
@@ -122,6 +126,14 @@
       />
       <el-table-column label="操作" align="center" min-width="120px">
         <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openDetail(scope.row.id)"
+            v-hasPermi="['iot:product:query']"
+          >
+            查看
+          </el-button>
           <el-button
             link
             type="primary"
@@ -157,8 +169,7 @@
 <script setup lang="ts">
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
-import { DeviceApi, DeviceUpdateStatusVO, DeviceVO } from '@/api/iot/device'
+import { DeviceApi, DeviceVO } from '@/api/iot/device'
 import DeviceForm from './DeviceForm.vue'
 import { ProductApi } from '@/api/iot/product'
 
@@ -223,6 +234,12 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
+/** 打开详情 */
+const { currentRoute, push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'IoTDeviceDetail', params: { id } })
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
@@ -235,21 +252,6 @@ const handleDelete = async (id: number) => {
     await getList()
   } catch {}
 }
-
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await DeviceApi.exportDevice(queryParams)
-    download.excel(data, '设备.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
 /** 查询字典下拉列表 */
 const products = ref()
 const getProducts = async () => {