无线电资产管理
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

584 lines
17 KiB

<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: 100%; overflow-y: auto;">
<el-card
v-for="(item, index) in warehouseCardList"
:key="index"
class="list-card"
:class="{ 'warning-active': item.isWarningActive }"
>
<div slot="header" class="store-info">
<img src="@/assets/images/tab_archives_logo.png" alt="" style="display: block; width: 50px; margin-right: 10px;">
<div style="flex: 1;">
<div class="store-info-item store-top">
<p style="width:62%;">{{ item.warehouse.warehouseName || '仓库' }}</p>
<p style="flex:1;">面积:{{ item.warehouse.warehouseArea || '0' }} ㎡</p>
<i
style="width:6%; text-align: center; cursor: pointer;"
class="iconfont icon-wangluoshexiangtou"
@click.stop="openWarehouseCamera(item.warehouse)"
/>
</div>
<div class="store-info-item" style="font-size: 12px; color: #909399; margin-top: 10px;">
<div style="display: flex; justify-content: flex-start;">
<p v-if="item.warehouse.administrator">{{ item.warehouse.administrator }}</p>
<p v-if="item.warehouse.contactPhone">&nbsp;{{ item.warehouse.contactPhone }}</p>
</div>
<p v-if="item.warehouse.lastTime">最后同步时间: {{ item.warehouse.lastTime | parseTime }}</p>
</div>
</div>
</div>
<ul class="store-monitor">
<li>
<span>当日进出</span>
<p>{{ item.todayInCount || 0 }}次</p>
</li>
<li>
<span>本月进出</span>
<p>{{ item.monthlyInCount || 0 }}次</p>
</li>
<li>
<span>本月入库</span>
<p>{{ item.monthlyInboundCount || 0 }}次</p>
</li>
<li>
<span>本月出库</span>
<p>{{ item.monthlyOutboundCount || 0 }}次</p>
</li>
<li>
<span>本月出入库异动</span>
<p>{{ item.fluctuationCount || 0 }}次</p>
</li>
<li>
<span>本月重点资产异动</span>
<p>{{ item.fluctuationImportantCount || 0 }}次</p>
</li>
</ul>
<div class="warning-text">
<i class="iconfont icon-baojingjilu" />
<div v-if="item.isWarningActive" :ref="el => setWarningRef(el, index)" class="warning-text-scroll">
<div class="scroll-wrap">
<div v-for="(warn, warnIdx) in item.warnList" :key="warnIdx" class="scroll-item">
{{ warn.warnValue }}
</div>
<!-- 复制一份,用于无缝循环 -->
<div v-for="(warn, warnIdx) in item.warnList" :key="'copy_'+warnIdx" class="scroll-item">
{{ warn.warnValue }}
</div>
</div>
</div>
<div v-else style="font-size: 12px; color: #999; flex: 1;">暂无报警信息</div>
<i class="el-icon-more" style="font-size: 12px; cursor: pointer;" @click.stop="handleMoreClick(item)" />
</div>
</el-card>
<div v-if="warehouseCardList.length === 0 && !crud.loading" class="no-device">
<el-empty style="padding:80px 0;" :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>
<!-- ==========新增:历史报警弹窗========== -->
<el-dialog
title="历史报警信息"
append-to-body
width="900px"
:visible.sync="alarmDialogVisible"
>
<el-table
ref="table"
class="archives-table"
stripe
style="width: 100%;"
height="calc(100vh - 530px)"
:data="alarmTableData"
>
<el-table-column prop="warehouseName" label="仓库名称" />
<el-table-column prop="warnValue" label="报警信息" />
<el-table-column prop="startTime" label="报警时间" width="160">
<template slot-scope="scope">
{{ scope.row.startTime | parseTime }}
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right;"
:current-page="alarmPageNum"
:page-size="alarmPageSize"
:total="alarmTotal"
layout="total, prev, pager, next"
@current-change="alarmPageChange"
/>
</el-dialog>
</div>
</template>
<script>
import crudDevice, { FetchFondsWarehouseTree, FetchDeviceAllByWarehouseId, FetchInitAlarmsLog } 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/warehouse/warehouseMonitoring',
idField: 'warehouse.id',
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,
warehouseCardList: [],
warningRefs: [],
warningIntervals: [],
warningScrollTasks: [],
hkVideoVisible: false,
hkConfig: {
username: null,
password: null,
ip: null,
port: null
},
// ==========历史报警弹窗新增data==========
alarmDialogVisible: false,
alarmLoading: false,
alarmWarehouseNo: '',
alarmTableData: [],
alarmPageNum: 1,
alarmPageSize: 10,
alarmTotal: 0
}
},
created() {
FetchFondsWarehouseTree().then(res => {
const filteredData = this.filterTreeData(res)
const topParent = {
id: 0,
name: '市州',
children: filteredData
}
this.warehouseTreeData = [topParent]
this.$nextTick(() => {
this.selectFirstWarehouseNode()
})
}).catch(() => {})
},
beforeDestroy() {
this.clearAllWarningScroll()
},
methods: {
[CRUD.HOOK.beforeRefresh]() {
this.crud.params.page = null
this.crud.params.size = null
},
[CRUD.HOOK.afterRefresh]() {
const apiList = this.crud.data || []
this.buildWarehouseCardList(apiList)
},
buildWarehouseCardList(apiResult) {
const apiList = this.crud.data || []
if (!Array.isArray(apiList) || apiList.length === 0) {
this.warehouseCardList = []
return
}
let filterList = []
if (this.selectedTreeKey?.id === 0) {
filterList = apiList
} else {
const selectFondsNo = this.selectedTreeKey?.fondsNo
filterList = apiList.filter(item => item.warehouse.fondsNo === selectFondsNo)
}
const cardList = filterList.map(item => {
return {
...item,
isWarningActive: Array.isArray(item.warnList) && item.warnList.length > 0
}
})
this.warehouseCardList = cardList
console.log('cardList', cardList)
this.$nextTick(() => {
this.startAllTextScroll()
})
},
setWarningRef(el, index) {
if (el) this.warningRefs[index] = el
},
clearAllWarningScroll() {
this.warningScrollTasks.forEach(task => {
if (task && typeof task.stop === 'function') task.stop()
})
this.warningScrollTasks = []
},
startAllTextScroll() {
this.clearAllWarningScroll()
this.warningRefs.forEach(el => {
if (!el) return
const wrapDom = el.querySelector('.scroll-wrap')
if (wrapDom) {
wrapDom.style.transform = `translateY(0px)`
wrapDom.style.transition = ''
}
})
this.warehouseCardList.forEach((card, idx) => {
if (!card.isWarningActive || !card.warnList || card.warnList.length <= 0) return
const boxDom = this.warningRefs[idx]
if (!boxDom) return
const wrapDom = boxDom.querySelector('.scroll-wrap')
if (!wrapDom) return
const itemHeight = 44
const originLen = card.warnList.length
const totalOriginHeight = originLen * itemHeight
let offset = 0
let timer = null
const scrollStep = () => {
wrapDom.style.transition = 'transform 0.6s ease-in-out'
offset += itemHeight
wrapDom.style.transform = `translateY(-${offset}px)`
timer = setTimeout(() => {
if (offset >= totalOriginHeight) {
wrapDom.style.transition = 'none'
offset = 0
wrapDom.style.transform = `translateY(0px)`
wrapDom.offsetHeight
}
scrollStep()
}, 3000)
}
const stop = () => {
clearTimeout(timer)
}
this.warningScrollTasks.push({ stop })
scrollStep()
})
},
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) {
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
},
async openWarehouseCamera(cardItem) {
const warehouseId = cardItem.id
try {
const res = await FetchDeviceAllByWarehouseId({ warehouseId })
const allDevices = res || []
const ballDevices = allDevices.filter(d => d.deviceTypeName === '网络高清球机')
if (ballDevices.length === 0) {
this.$message.info('该仓库暂无网络高清球机设备')
return
}
console.log('当前仓库匹配的球机', ballDevices)
this.handleViewVideo(ballDevices[0].id)
} catch (err) {
console.error('获取仓库设备异常:', err)
this.$message.error('获取仓库设备失败')
}
},
async handleViewVideo(deviceId) {
try {
const [detailRes, pwdRes] = await Promise.all([
crudDevice.FetchDeviceDetailsById({ deviceId }),
crudDevice.FetchDevicePasswordById({ deviceId })
])
if (!detailRes) return
const finalPassword = pwdRes || detailRes.devicePassword
this.hkConfig = {
username: detailRes.deviceAccount,
password: finalPassword,
ip: detailRes.deviceIp,
port: detailRes.devicePort
}
this.hkVideoVisible = true
this.$nextTick(() => {
this.$refs.hkVideoRef.initVideo()
})
} catch (err) {
console.error('打开视频失败:', err)
this.$message.error('获取监控设备信息失败')
}
},
// 修改后 handleMoreClick
handleMoreClick(item) {
this.alarmWarehouseNo = item.warehouse.warehouseNo
this.alarmPageNum = 1
this.alarmDialogVisible = true
this.getAlarmList()
},
// 获取报警列表
async getAlarmList() {
this.alarmLoading = true
try {
const param = {
warehouseNo: this.alarmWarehouseNo,
pageNum: this.alarmPageNum,
pageSize: this.alarmPageSize
}
const res = await FetchInitAlarmsLog(param)
this.alarmTableData = res.records || []
this.alarmTotal = res.total || 0
} catch (e) {
this.$message.error('查询历史报警失败')
} finally {
this.alarmLoading = false
}
},
// 分页切换
alarmPageChange(page) {
this.alarmPageNum = page
this.getAlarmList()
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-tree {
.el-tree-node__content {
font-size: 14px;
color: #545B65;
}
.el-tree-node__children {
.el-tree-node__content {
font-size: 14px;
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 {
height: 66px;
padding: 12px 10px !important;
border-radius: 10px 10px 0 0;
color: #0c0e1e;
font-size: 16px;
font-weight: 600;
background-color: #fff !important;
border-bottom: 1px solid #ebeef5 !important;
.store-info {
display: flex;
justify-content: flex-start;
align-items: center;
.store-info-item {
display: flex;
justify-content: space-between;
align-items: center;
p {
margin-right: 6px;
}
&.store-top {
justify-content: space-between;
}
}
}
}
.el-card__body {
padding: 5px 10px;
border-radius: 0 0 10px 10px;
.store-monitor {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: flex-start;
padding: 5px 5px 10px 5px;
line-height: 35px;
font-size: 14px;
li {
width: calc(50% - 10px);
display: flex;
justify-content: space-between;
align-items: center;
&:nth-child(even) {
margin-left: 15px;
}
span {
display: block;
width: 120px;
}
p {
flex: 1;
text-align: right;
font-weight: bold;
color: #0C0E1E;
}
}
}
}
.warning-text {
font-size: 14px;
font-weight: 600;
border-top: 1px solid #ebeef5;
display: flex;
align-items: center;
gap: 6px;
height: 44px;
overflow: hidden;
padding: 0 10px;
.iconfont {
flex-shrink: 0;
color: #F65163;
}
.warning-text-scroll {
flex: 1;
height: 44px;
overflow: hidden;
position: relative;
}
.scroll-wrap {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.scroll-item {
height: 44px;
line-height: 44px;
white-space: nowrap;
}
}
&.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;
}
}
}
.view-video {
::v-deep .el-dialog {
width: 1000px !important;
}
}
</style>