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

pull/542/head
安浩浩 2024-09-22 15:42:20 +08:00
parent 63a0e5dc3d
commit 93a0789e34
6 changed files with 387 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 () => {