|
|
<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"> {{ 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>
|