4 changed files with 115 additions and 608 deletions
-
161src/views/assetManage/monitor/index.vue
-
523src/views/assetManage/monitor/index1.vue
-
25src/views/home.vue
-
8src/views/system/user/index.vue
@ -1,523 +0,0 @@ |
|||
<template> |
|||
<div class="app-container"> |
|||
<div class="container-main" style="justify-content: flex-start;"> |
|||
<div class="elect-cont-left"> |
|||
<el-scrollbar style="height: calc(100vh - 198px);"> |
|||
<el-tree |
|||
ref="tree" |
|||
:data="warehouseTreeData" |
|||
:props="defaultProps" |
|||
node-key="id" |
|||
default-expand-all |
|||
:default-checked-keys="defaultCheckedKeys" |
|||
:expand-on-click-node="false" |
|||
:highlight-current="true" |
|||
@node-click="handleNodeClick" |
|||
/> |
|||
</el-scrollbar> |
|||
</div> |
|||
<!--用户数据--> |
|||
<div class="elect-cont-right" style="background-color: transparent; padding: 0 !important;"> |
|||
<div class="container-right" style="height: calc(100vh - 158px) !important;background-color: transparent !important; "> |
|||
<span class="right-top-line" /> |
|||
<span class="left-bottom-line" /> |
|||
<div class="monitor-list" style="height: calc(100%); overflow: hidden; overflow-y: scroll;"> |
|||
<el-card |
|||
v-for="(item, index) in warehouseCardList" |
|||
:key="index" |
|||
class="list-card" |
|||
:class="{ 'warning-active': item.isWarningActive }" |
|||
> |
|||
<div slot="header" class="store-info"> |
|||
<div class="store-info-item"> |
|||
<i class="iconfont icon-kufangguanli" /> |
|||
<p>{{ item.warehouseName || '仓库' }} </p> |
|||
<p>面积:{{ item.warehouseArea || '0' }} ㎡</p> |
|||
</div> |
|||
<div v-if="item.administrator || item.contactPhone" class="store-info-item"> |
|||
<i class="iconfont icon-yonghuguanli" /> |
|||
<p v-if="item.administrator">{{ item.administrator }}</p> |
|||
<p v-if="item.contactPhone">{{ item.contactPhone }}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<ul v-if="item.deviceList && item.deviceList.length > 0" v-loading="crud.loading" class="device-monitor-list"> |
|||
<li v-for="device in item.deviceList" :key="device.id"> |
|||
<i :class="getDeviceIcon(device.deviceTypeName)" :style="getDeviceIconStyle(device.deviceTypeName)" /> |
|||
<p>{{ device.deviceName }}</p> |
|||
<i |
|||
v-if="device.deviceTypeName === '网络高清球机'" |
|||
class="iconfont icon-yulan" |
|||
style="margin-right: 4px; cursor: pointer;" |
|||
@click="handleViewVideo(device.id)" |
|||
/> |
|||
</li> |
|||
</ul> |
|||
<!-- 空数据 --> |
|||
<div v-else class="no-device"> |
|||
<el-empty style="display: flex; flex-direction: column; align-items: center; height: 200px; padding: 0 !important;" :image-size="60" description="暂无设备" /> |
|||
</div> |
|||
|
|||
<div v-if="item.isWarningActive" class="warning-text"> |
|||
<i class="iconfont icon-baojingjilu" /> |
|||
<div class="warning-text-scroll" :class="{ scroll: item.warningNeedScroll }"> |
|||
<span :ref="el => setWarningRef(el, index)" class="scroll-text">{{ item.warningText }}</span> |
|||
</div> |
|||
</div> |
|||
</el-card> |
|||
<!-- 只有一个仓库都没有时才显示 --> |
|||
<div v-if="warehouseCardList.length === 0 && !crud.loading" class="no-device" style="width: 100%; height: 100%;"> |
|||
<el-empty style="display: flex; flex-direction: column; align-items: center; padding: 0 !important; height: 100%;" :image-size="60" description="暂无仓库信息" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 查看监控视频 --> |
|||
<el-dialog class="view-video" append-to-body :close-on-click-modal="false" :modal-append-to-body="false" :visible="hkVideoVisible" title="查看监控视频" :before-close="handleClose"> |
|||
<span class="dialog-right-top" /> |
|||
<span class="dialog-left-bottom" /> |
|||
<div class="setting-dialog"> |
|||
<hkVideo ref="hkVideoRef" :hk-config="hkConfig" /> |
|||
</div> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import crudDevice, { FetchFondsWarehouseTree, FetchWarehouseTree } from '@/api/assetDevice/index' |
|||
import CRUD, { presenter, header, crud } from '@crud/crud' |
|||
import hkVideo from '@/views/components/hkVideo.vue' |
|||
|
|||
export default { |
|||
name: 'Monitor', |
|||
components: { hkVideo }, |
|||
cruds() { |
|||
return CRUD({ |
|||
title: '设备监控', |
|||
url: 'api/device/getDeviceAllByWarehouseId', |
|||
idField: 'id || deviceId', |
|||
sort: [], |
|||
crudMethod: { ...crudDevice }, |
|||
optShow: { |
|||
add: false, |
|||
edit: false, |
|||
del: false, |
|||
reset: true, |
|||
download: false, |
|||
group: false |
|||
}, |
|||
queryOnPresenterCreated: false |
|||
}) |
|||
}, |
|||
mixins: [presenter(), header(), crud()], |
|||
data() { |
|||
return { |
|||
permission: { add: [], edit: [], del: [] }, |
|||
warehouseTreeData: null, |
|||
defaultProps: { |
|||
children: 'children', |
|||
label: 'name' |
|||
}, |
|||
defaultCheckedKeys: [], |
|||
selectedTreeKey: null, |
|||
storeData: [], |
|||
// 卡片列表(核心修改) |
|||
warehouseCardList: [], |
|||
warningRefs: [], // 多卡片滚动引用 |
|||
hkVideoVisible: false, |
|||
hkConfig: { |
|||
username: null, |
|||
password: null, |
|||
ip: null, |
|||
port: null |
|||
} |
|||
} |
|||
}, |
|||
created() { |
|||
// 获取仓库树 |
|||
FetchFondsWarehouseTree().then(res => { |
|||
const filteredData = this.filterTreeData(res) |
|||
const topParent = { |
|||
id: 0, |
|||
name: '市州', |
|||
children: filteredData |
|||
} |
|||
this.warehouseTreeData = [topParent] |
|||
this.$nextTick(() => { |
|||
this.selectFirstWarehouseNode() |
|||
}) |
|||
}).catch(() => {}) |
|||
|
|||
// 获取所有仓库数据 |
|||
this.getWarehouseTree() |
|||
}, |
|||
mounted() {}, |
|||
methods: { |
|||
[CRUD.HOOK.beforeRefresh](crud) { |
|||
this.crud.params.page = null |
|||
this.crud.params.size = null |
|||
}, |
|||
[CRUD.HOOK.afterRefresh](crud) { |
|||
// 数据回来后 → 按仓库分组 + 匹配信息 + 渲染卡片 |
|||
this.buildWarehouseCardList(crud.data || []) |
|||
}, |
|||
|
|||
// 根据设备类型返回图标 |
|||
getDeviceIcon(typeName) { |
|||
const iconMap = { |
|||
'RFID吸顶式安全门': 'iconfont icon-saomiao', |
|||
'RFID固定式读写器': 'iconfont icon-rfidduxieqi', |
|||
'RFID手持盘点终端': 'iconfont icon-shouchipandianyi', |
|||
'网络高清球机': 'iconfont icon-wangluoshexiangtou', |
|||
'智能一体机': 'iconfont icon-yitiji', |
|||
'人脸识别门禁一体机': 'iconfont icon-renlianshibiejiqi' |
|||
} |
|||
return iconMap[typeName] || 'iconfont icon-default' |
|||
}, |
|||
|
|||
// 根据设备类型返回图标大小 |
|||
getDeviceIconStyle(typeName) { |
|||
const styleMap = { |
|||
'RFID吸顶式安全门': { fontSize: '12px' }, |
|||
'RFID固定式读写器': { fontSize: '20px' }, |
|||
'网络高清球机': { fontSize: '18px' }, |
|||
'人脸识别门禁一体机': { fontSize: '18px' }, |
|||
'RFID手持盘点终端': { fontSize: '18px' } |
|||
} |
|||
return styleMap[typeName] || { fontSize: '16px' } |
|||
}, |
|||
|
|||
getWarehouseTree() { |
|||
FetchWarehouseTree().then(res => { |
|||
this.storeData = res || [] |
|||
// 测试使用 |
|||
// this.storeData = [] |
|||
// this.storeData[4].lastSyncTime = '2026-04-03T08:33:19.014+00:00' |
|||
// this.storeData[4].syncExplanation = '报警文案报警文案报警文案报警文案报警文案报警文案报警文案报警文案报警文案报警文案' |
|||
|
|||
// this.storeData[2].lastSyncTime = '2026-04-03T08:33:19.014+00:00' |
|||
// this.storeData[2].syncExplanation = 'ddd' |
|||
// console.log('storeData', this.storeData) |
|||
}).catch(() => {}) |
|||
}, |
|||
// 获取当前节点下所有仓库 → 生成卡片(无论有没有设备都显示) |
|||
buildWarehouseCardList(deviceList) { |
|||
// 1. 没有仓库数据直接清空 |
|||
if (!this.storeData.length) { |
|||
this.warehouseCardList = [] |
|||
return |
|||
} |
|||
|
|||
// 2. 拿到当前选中的市州节点 |
|||
const currentFondsNo = this.selectedTreeKey?.fondsNo |
|||
|
|||
// 3. 筛选出【当前选中市州下的所有仓库】(关键:不管有没有设备都显示) |
|||
let allWarehousesInCurrentFonds = [] |
|||
if (this.selectedTreeKey.id === 0) { |
|||
// 选中根节点“市州” → 显示所有仓库 |
|||
allWarehousesInCurrentFonds = this.storeData |
|||
} else { |
|||
// 选中具体市州 → 只显示该市州下的仓库 |
|||
allWarehousesInCurrentFonds = this.storeData.filter( |
|||
s => s.fondsNo === currentFondsNo |
|||
) |
|||
} |
|||
|
|||
// 4. 把设备按仓库分组(方便匹配) |
|||
const deviceGroup = {} |
|||
deviceList.forEach(item => { |
|||
const key = item.fondsNo + '_' + item.warehouseNo |
|||
if (!deviceGroup[key]) deviceGroup[key] = [] |
|||
deviceGroup[key].push(item) |
|||
}) |
|||
|
|||
// 5. 遍历所有仓库 → 生成卡片(重点:全部生成) |
|||
const cardList = [] |
|||
allWarehousesInCurrentFonds.forEach(warehouse => { |
|||
const key = warehouse.fondsNo + '_' + warehouse.warehouseNo |
|||
const deviceListOfWarehouse = deviceGroup[key] || [] // 有设备取设备,没有就是空数组 |
|||
|
|||
let isWarningActive = false |
|||
let warningText = '' |
|||
const lastSyncTime = warehouse.lastSyncTime |
|||
const syncExplanation = warehouse.syncExplanation |
|||
|
|||
if (lastSyncTime) { |
|||
try { |
|||
const now = new Date().getTime() |
|||
const syncTime = new Date(lastSyncTime).getTime() |
|||
if (!isNaN(syncTime)) { |
|||
const diffMinutes = (now - syncTime) / (1000 * 60) |
|||
if (diffMinutes > 10) { |
|||
isWarningActive = true |
|||
warningText = syncExplanation || '同步超时超过10分钟' |
|||
} |
|||
} |
|||
} catch (e) { |
|||
console.log(e) |
|||
} |
|||
} |
|||
|
|||
cardList.push({ |
|||
...warehouse, |
|||
deviceList: deviceListOfWarehouse, |
|||
isWarningActive, |
|||
warningText, |
|||
warningNeedScroll: false |
|||
}) |
|||
}) |
|||
|
|||
this.warehouseCardList = cardList |
|||
|
|||
// 6. 启动报警滚动 |
|||
this.$nextTick(() => { |
|||
this.startAllTextScroll() |
|||
}) |
|||
}, |
|||
|
|||
// 保存多个警告文字DOM |
|||
setWarningRef(el, index) { |
|||
if (el) this.warningRefs[index] = el |
|||
}, |
|||
|
|||
// 所有报警条横向滚动 |
|||
startAllTextScroll() { |
|||
this.warehouseCardList.forEach((item, idx) => { |
|||
if (!item.isWarningActive) return |
|||
const scrollText = this.warningRefs[idx] |
|||
if (!scrollText) return |
|||
const scrollBox = scrollText.parentElement |
|||
if (!scrollBox) return |
|||
|
|||
const textWidth = scrollText.scrollWidth |
|||
const boxWidth = scrollBox.clientWidth |
|||
this.warehouseCardList[idx].warningNeedScroll = textWidth > boxWidth |
|||
|
|||
if (textWidth <= boxWidth) return |
|||
|
|||
let pos = 0 |
|||
const speed = 1 |
|||
setInterval(() => { |
|||
pos -= speed |
|||
if (pos < -textWidth) pos = boxWidth |
|||
scrollText.style.transform = `translateX(${pos}px)` |
|||
}, 30) |
|||
}) |
|||
}, |
|||
|
|||
// 过滤树数据 |
|||
filterTreeData(tree) { |
|||
return tree.map(node => { |
|||
if (node.type === 'warehouse') return null |
|||
const newNode = { ...node } |
|||
if (newNode.children && newNode.children.length) { |
|||
newNode.children = this.filterTreeData(newNode.children).filter(item => item) |
|||
} |
|||
return newNode |
|||
}).filter(item => item) |
|||
}, |
|||
|
|||
// // 默认选中第一个市州 |
|||
// selectFirstWarehouseNode() { |
|||
// const root = this.warehouseTreeData[0] |
|||
// if (root && root.children && root.children.length) { |
|||
// const firstFonds = root.children[0] |
|||
// this.defaultCheckedKeys = [firstFonds.id] |
|||
// this.$refs.tree.setCurrentKey(firstFonds.id) |
|||
// this.handleNodeClick(firstFonds) |
|||
// } |
|||
// }, |
|||
// 默认选中【市州】根节点 |
|||
selectFirstWarehouseNode() { |
|||
const root = this.warehouseTreeData[0] |
|||
if (root) { |
|||
// 直接选中根节点 id = 0 |
|||
this.defaultCheckedKeys = [root.id] |
|||
this.$refs.tree.setCurrentKey(root.id) |
|||
this.handleNodeClick(root) |
|||
} |
|||
}, |
|||
|
|||
// 点击树节点 |
|||
handleNodeClick(data) { |
|||
this.selectedTreeKey = data |
|||
if (this.selectedTreeKey.id === 0) { |
|||
this.crud.query.warehouseId = null |
|||
this.crud.query.fondsId = null |
|||
} else { |
|||
this.crud.query.fondsId = data.id |
|||
} |
|||
this.search = '' |
|||
this.crud.toQuery() |
|||
}, |
|||
|
|||
// 关闭弹窗 |
|||
handleClose() { |
|||
this.hkVideoVisible = false |
|||
}, |
|||
|
|||
handleViewVideo(data) { |
|||
const deviceId = data |
|||
Promise.all([ |
|||
crudDevice.FetchDeviceDetailsById({ deviceId }), |
|||
crudDevice.FetchDevicePasswordById({ deviceId }) |
|||
]).then(([detailRes, pwdRes]) => { |
|||
if (detailRes) { |
|||
const finalPassword = pwdRes || detailRes.devicePassword |
|||
this.hkConfig = { |
|||
username: detailRes.deviceAccount, |
|||
devicePassword: finalPassword, |
|||
ip: detailRes.deviceIp, |
|||
port: detailRes.devicePort |
|||
} |
|||
this.hkVideoVisible = true |
|||
this.$nextTick(() => { |
|||
this.$refs.hkVideoRef.initVideo() |
|||
}) |
|||
} |
|||
}).catch(err => { |
|||
console.log('获取设备视频信息失败', err) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
::v-deep .el-tree{ |
|||
.el-tree-node__content{ |
|||
font-size: 14px; |
|||
color: #545B65; |
|||
.tree-text{ |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
color: #0C0E1E; |
|||
} |
|||
} |
|||
.el-tree-node__children{ |
|||
.tree-text { |
|||
font-size: 14px !important; |
|||
font-weight: normal; |
|||
text-overflow: ellipsis; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
display: inline-block; |
|||
min-width: 170px; |
|||
color: #545B65; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.monitor-list{ |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
align-items: flex-start; |
|||
flex-wrap: wrap; |
|||
padding-top: 15px; |
|||
.list-card{ |
|||
width: calc(100% / 3 - 20px); |
|||
margin: 0 10px 20px 10px; |
|||
} |
|||
} |
|||
::v-deep .el-card { |
|||
border-radius: 10px; |
|||
.el-card__header{ |
|||
padding: 12px 10px !important; |
|||
border-radius: 10px 10px 0 0; |
|||
color: #0c0e1e; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
background-color: #fff !important; |
|||
border-bottom: 1px solid #ebeef5 !important; |
|||
.store-info{ |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
.store-info-item{ |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
p{ |
|||
margin-right: 6px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.el-card__body{ |
|||
border-radius: 0 0 10px 10px; |
|||
.device-monitor-list{ |
|||
height: 200px; |
|||
padding: 10px; |
|||
line-height: 30px; |
|||
font-size: 14px; |
|||
overflow: hidden; |
|||
overflow-y: scroll; |
|||
li{ |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
.iconfont{ |
|||
width: 22px; |
|||
text-align: center; |
|||
} |
|||
p{ |
|||
flex: 1; |
|||
padding: 0 6px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.warning-text{ |
|||
color: #F65163; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
border-top: 1px solid #ebeef5 !important; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
height: 44px; |
|||
overflow: hidden; |
|||
padding: 0 10px !important; |
|||
|
|||
.iconfont{ |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.warning-text-scroll { |
|||
flex: 1; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
line-height: 44px; |
|||
position: relative; |
|||
} |
|||
|
|||
.scroll-text { |
|||
display: inline-block; |
|||
white-space: nowrap; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
} |
|||
} |
|||
|
|||
&.warning-active{ |
|||
.el-card__header{ |
|||
background-color: rgba(85, 85, 85, 0.2) !important; |
|||
border-bottom: 1px solid #fff !important; |
|||
} |
|||
.el-card__body{ |
|||
background-color: rgba(85, 85, 85, 0.2) !important; |
|||
} |
|||
.warning-text{ |
|||
border-top: 1px solid #fff !important; |
|||
} |
|||
} |
|||
} |
|||
.view-video{ |
|||
::v-deep .el-dialog{ |
|||
width: 1000px !important; |
|||
} |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue