Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
93a0789e
authored
Sep 22, 2024
by
安浩浩
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【新增】 IOT 设备管理,设备详情
parent
63a0e5dc
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
387 additions
and
18 deletions
+387
-18
src/api/iot/device/index.ts
+1
-0
src/router/modules/remaining.ts
+11
-0
src/views/iot/device/detail/DeviceDetailsHeader.vue
+114
-0
src/views/iot/device/detail/DeviceDetailsInfo.vue
+175
-0
src/views/iot/device/detail/index.vue
+66
-0
src/views/iot/device/index.vue
+20
-18
No files found.
src/api/iot/device/index.ts
View file @
93a0789e
...
@@ -15,6 +15,7 @@ export interface DeviceVO {
...
@@ -15,6 +15,7 @@ export interface DeviceVO {
lastOnlineTime
:
Date
// 最后上线时间
lastOnlineTime
:
Date
// 最后上线时间
lastOfflineTime
:
Date
// 最后离线时间
lastOfflineTime
:
Date
// 最后离线时间
activeTime
:
Date
// 设备激活时间
activeTime
:
Date
// 设备激活时间
createTime
:
Date
// 创建时间
ip
:
string
// 设备的 IP 地址
ip
:
string
// 设备的 IP 地址
firmwareVersion
:
string
// 设备的固件版本
firmwareVersion
:
string
// 设备的固件版本
deviceSecret
:
string
// 设备密钥,用于设备认证,需安全存储
deviceSecret
:
string
// 设备密钥,用于设备认证,需安全存储
...
...
src/router/modules/remaining.ts
View file @
93a0789e
...
@@ -622,6 +622,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
...
@@ -622,6 +622,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
activeMenu
:
'/iot/product'
activeMenu
:
'/iot/product'
},
},
component
:
()
=>
import
(
'@/views/iot/product/detail/index.vue'
)
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'
)
}
}
]
]
}
}
...
...
src/views/iot/device/detail/DeviceDetailsHeader.vue
0 → 100644
View file @
93a0789e
<
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
>
src/views/iot/device/detail/DeviceDetailsInfo.vue
0 → 100644
View file @
93a0789e
<
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
>
src/views/iot/device/detail/index.vue
0 → 100644
View file @
93a0789e
<
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
>
src/views/iot/device/index.vue
View file @
93a0789e
...
@@ -96,7 +96,11 @@
...
@@ -96,7 +96,11 @@
<!-- 列表 -->
<!-- 列表 -->
<ContentWrap>
<ContentWrap>
<el-table
v-loading=
"loading"
:data=
"list"
:stripe=
"true"
:show-overflow-tooltip=
"true"
>
<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=
"nickname"
/>
<el-table-column
label=
"设备所属产品"
align=
"center"
prop=
"productId"
>
<el-table-column
label=
"设备所属产品"
align=
"center"
prop=
"productId"
>
<
template
#
default=
"scope"
>
<
template
#
default=
"scope"
>
...
@@ -125,6 +129,14 @@
...
@@ -125,6 +129,14 @@
<el-button
<el-button
link
link
type=
"primary"
type=
"primary"
@
click=
"openDetail(scope.row.id)"
v-hasPermi=
"['iot:product:query']"
>
查看
</el-button>
<el-button
link
type=
"primary"
@
click=
"openForm('update', scope.row.id)"
@
click=
"openForm('update', scope.row.id)"
v-hasPermi=
"['iot:device:update']"
v-hasPermi=
"['iot:device:update']"
>
>
...
@@ -157,8 +169,7 @@
...
@@ -157,8 +169,7 @@
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
{
DICT_TYPE
,
getIntDictOptions
}
from
'@/utils/dict'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
download
from
'@/utils/download'
import
{
DeviceApi
,
DeviceVO
}
from
'@/api/iot/device'
import
{
DeviceApi
,
DeviceUpdateStatusVO
,
DeviceVO
}
from
'@/api/iot/device'
import
DeviceForm
from
'./DeviceForm.vue'
import
DeviceForm
from
'./DeviceForm.vue'
import
{
ProductApi
}
from
'@/api/iot/product'
import
{
ProductApi
}
from
'@/api/iot/product'
...
@@ -223,6 +234,12 @@ const openForm = (type: string, id?: number) => {
...
@@ -223,6 +234,12 @@ const openForm = (type: string, id?: number) => {
formRef
.
value
.
open
(
type
,
id
)
formRef
.
value
.
open
(
type
,
id
)
}
}
/** 打开详情 */
const
{
currentRoute
,
push
}
=
useRouter
()
const
openDetail
=
(
id
:
number
)
=>
{
push
({
name
:
'IoTDeviceDetail'
,
params
:
{
id
}
})
}
/** 删除按钮操作 */
/** 删除按钮操作 */
const
handleDelete
=
async
(
id
:
number
)
=>
{
const
handleDelete
=
async
(
id
:
number
)
=>
{
try
{
try
{
...
@@ -235,21 +252,6 @@ const handleDelete = async (id: number) => {
...
@@ -235,21 +252,6 @@ const handleDelete = async (id: number) => {
await
getList
()
await
getList
()
}
catch
{}
}
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
products
=
ref
()
const
getProducts
=
async
()
=>
{
const
getProducts
=
async
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment