|
|
|
@ -16,12 +16,12 @@ |
|
|
|
/> |
|
|
|
</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; "> |
|
|
|
<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;"> |
|
|
|
<div class="monitor-list" style="height: 100%; overflow-y: auto;"> |
|
|
|
<el-card |
|
|
|
v-for="(item, index) in warehouseCardList" |
|
|
|
:key="index" |
|
|
|
@ -32,21 +32,20 @@ |
|
|
|
<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.warehouseName || '仓库' }} </p> |
|
|
|
<p style="flex:1;">面积:{{ item.warehouseArea || '0' }} ㎡</p> |
|
|
|
<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)" |
|
|
|
@click.stop="openWarehouseCamera(item.warehouse)" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div class="store-info-item" style="font-size: 12px; color: #909399; margin-top: 10px;"> |
|
|
|
<!-- <i class="iconfont icon-yonghuguanli" /> --> |
|
|
|
<div style="display: flex; justify-content: flex-start;"> |
|
|
|
<p v-if="item.administrator">{{ item.administrator }}</p> |
|
|
|
<p v-if="item.contactPhone">{{ item.contactPhone }}</p> |
|
|
|
<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.lastSyncTime">最后同步时间: {{ item.lastSyncTime | parseTime }}</p> |
|
|
|
<p v-if="item.warehouse.lastTime">最后同步时间: {{ item.warehouse.lastTime | parseTime }}</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -54,48 +53,64 @@ |
|
|
|
<ul class="store-monitor"> |
|
|
|
<li> |
|
|
|
<span>当日进出</span> |
|
|
|
<p>{{ item.todayInCount || '0' }}次</p> |
|
|
|
<p>{{ item.todayInCount || 0 }}次</p> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<span>本月进出</span> |
|
|
|
<p>{{ item.todayInCount || '0' }}次</p> |
|
|
|
<p>{{ item.monthlyInCount || 0 }}次</p> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<span>本月入库</span> |
|
|
|
<p>{{ item.todayInCount || '0' }}次</p> |
|
|
|
<p>{{ item.monthlyInboundCount || 0 }}次</p> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<span>本月出库</span> |
|
|
|
<p>{{ item.todayInCount || '0' }}次</p> |
|
|
|
<p>{{ item.monthlyOutboundCount || 0 }}次</p> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<span>本月出入库异动</span> |
|
|
|
<p>{{ item.todayInCount || '0' }}次</p> |
|
|
|
<p>{{ item.fluctuationCount || 0 }}次</p> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<span>本月重点资产异动</span> |
|
|
|
<p>{{ item.todayInCount || '0' }}次</p> |
|
|
|
<p>{{ item.fluctuationImportantCount || 0 }}次</p> |
|
|
|
</li> |
|
|
|
</ul> |
|
|
|
|
|
|
|
<div class="warning-text"> |
|
|
|
<i v-if="item.isWarningActive" class="iconfont icon-baojingjilu" /> |
|
|
|
<div v-if="item.isWarningActive" class="warning-text-scroll" :class="{ scroll: item.warningNeedScroll }"> |
|
|
|
<span :ref="el => setWarningRef(el, index)" class="scroll-text">{{ item.warningText }}</span> |
|
|
|
<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> |
|
|
|
</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 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"> |
|
|
|
<!-- 监控视频弹窗 --> |
|
|
|
<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"> |
|
|
|
@ -106,24 +121,20 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import crudDevice, { FetchFondsWarehouseTree, FetchWarehouseTree } from '@/api/assetDevice/index' |
|
|
|
import crudDevice, { FetchFondsWarehouseTree, FetchDeviceAllByWarehouseId } from '@/api/assetDevice/index' |
|
|
|
import CRUD, { presenter, header, crud } from '@crud/crud' |
|
|
|
import hkVideo from '@/views/components/hkVideo.vue' |
|
|
|
// import { swiper, swiperSlide } from 'vue-awesome-swiper' |
|
|
|
// import 'swiper/dist/css/swiper.css' |
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'Monitor', |
|
|
|
components: { |
|
|
|
hkVideo |
|
|
|
// swiper, |
|
|
|
// swiperSlide |
|
|
|
}, |
|
|
|
cruds() { |
|
|
|
return CRUD({ |
|
|
|
title: '设备监控', |
|
|
|
url: 'api/device/getDeviceAllByWarehouseId', |
|
|
|
idField: 'id || deviceId', |
|
|
|
title: '站点监控', |
|
|
|
url: 'api/warehouse/warehouseMonitoring', |
|
|
|
idField: 'warehouse.id', |
|
|
|
sort: [], |
|
|
|
crudMethod: { ...crudDevice }, |
|
|
|
optShow: { |
|
|
|
@ -148,35 +159,20 @@ export default { |
|
|
|
}, |
|
|
|
defaultCheckedKeys: [], |
|
|
|
selectedTreeKey: null, |
|
|
|
storeData: [], |
|
|
|
// 卡片列表(核心修改) |
|
|
|
warehouseCardList: [], |
|
|
|
warningRefs: [], // 多卡片滚动引用 |
|
|
|
warningRefs: [], |
|
|
|
warningIntervals: [], |
|
|
|
warningScrollTasks: [], |
|
|
|
hkVideoVisible: false, |
|
|
|
hkConfig: { |
|
|
|
username: null, |
|
|
|
password: null, |
|
|
|
ip: null, |
|
|
|
port: null |
|
|
|
}, |
|
|
|
swiperOptionContent: { |
|
|
|
slidesPerView: 1, // 一屏显示 1 组 |
|
|
|
slidesPerColumn: 1, // 只显示一组 |
|
|
|
allowTouchMove: true, // 允许滑动 |
|
|
|
loop: false, // 不需要循环 |
|
|
|
autoplay: { |
|
|
|
delay: 6000, // 3秒切换一次 |
|
|
|
disableOnInteraction: false // 操作后继续自动播 |
|
|
|
}, |
|
|
|
pagination: { // 分页指示器(可选) |
|
|
|
el: '.swiper-pagination', |
|
|
|
clickable: true |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
created() { |
|
|
|
// 获取仓库树 |
|
|
|
FetchFondsWarehouseTree().then(res => { |
|
|
|
const filteredData = this.filterTreeData(res) |
|
|
|
const topParent = { |
|
|
|
@ -189,203 +185,156 @@ export default { |
|
|
|
this.selectFirstWarehouseNode() |
|
|
|
}) |
|
|
|
}).catch(() => {}) |
|
|
|
|
|
|
|
// 获取所有仓库数据 |
|
|
|
this.getWarehouseTree() |
|
|
|
}, |
|
|
|
mounted() {}, |
|
|
|
beforeDestroy() { |
|
|
|
this.clearAllWarningScroll() |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
[CRUD.HOOK.beforeRefresh](crud) { |
|
|
|
[CRUD.HOOK.beforeRefresh]() { |
|
|
|
this.crud.params.page = null |
|
|
|
this.crud.params.size = null |
|
|
|
}, |
|
|
|
[CRUD.HOOK.afterRefresh](crud) { |
|
|
|
// 数据回来后 → 按仓库分组 + 匹配信息 + 渲染卡片 |
|
|
|
this.buildWarehouseCardList(crud.data || []) |
|
|
|
}, |
|
|
|
// 设备列表按 6 个一组分屏(2行3列) |
|
|
|
getDeviceSlides(deviceList) { |
|
|
|
const pageSize = 6 |
|
|
|
const slides = [] |
|
|
|
for (let i = 0; i < deviceList.length; i += pageSize) { |
|
|
|
slides.push(deviceList.slice(i, i + pageSize)) |
|
|
|
} |
|
|
|
return slides |
|
|
|
}, |
|
|
|
getDeviceListBg(typeName) { |
|
|
|
const styleMap = { |
|
|
|
'RFID吸顶式安全门': { borderLeft: '2px solid rgba(3, 72, 243, 1)' }, |
|
|
|
'RFID固定式读写器': { borderLeft: '2px solid rgba(20, 201, 201, 1)' }, |
|
|
|
'RFID手持盘点终端': { borderLeft: '2px solid rgba(248, 183, 34, 1)' }, |
|
|
|
'网络高清球机': { borderLeft: '2px solid rgba(114, 46, 209, 1)' }, |
|
|
|
'智能一体机': { borderLeft: '2px solid rgba(244, 100, 123, 1)' }, |
|
|
|
'人脸识别门禁一体机': { borderLeft: '2px solid rgba(46, 125, 50, 1)' } |
|
|
|
|
|
|
|
// 'RFID吸顶式安全门': { borderLeft: '2px solid rgba(3, 72, 243, 1)' }, |
|
|
|
// 'RFID固定式读写器': { borderLeft: '2px solid rgba(3, 72, 243, 1)' }, |
|
|
|
// 'RFID手持盘点终端': { borderLeft: '2px solid rgba(3, 72, 243, 1)' }, |
|
|
|
// '网络高清球机': { borderLeft: '2px solid rgba(3, 72, 243, 1)' }, |
|
|
|
// '智能一体机': { borderLeft: '2px solid rgba(3, 72, 243, 1)' }, |
|
|
|
// '人脸识别门禁一体机': { borderLeft: '2px solid rgba(3, 72, 243, 1)' } |
|
|
|
} |
|
|
|
return styleMap[typeName] || { borderLeft: '2px solid rgba(20, 201, 201, 1)' } |
|
|
|
}, |
|
|
|
getDeviceIconBg(typeName) { |
|
|
|
const styleMap = { |
|
|
|
'RFID吸顶式安全门': { backgroundColor: 'rgba(3, 72, 243, 0.1)' }, |
|
|
|
'RFID固定式读写器': { backgroundColor: 'rgba(20, 201, 201, 0.1)' }, |
|
|
|
'RFID手持盘点终端': { backgroundColor: 'rgba(248, 183, 34, 0.1)' }, |
|
|
|
'网络高清球机': { backgroundColor: 'rgba(114, 46, 209, 0.1)' }, |
|
|
|
'智能一体机': { backgroundColor: 'rgba(244, 100, 123, 0.1)' }, |
|
|
|
'人脸识别门禁一体机': { backgroundColor: 'rgba(46, 125, 50, 0.1)' } |
|
|
|
} |
|
|
|
return styleMap[typeName] || { backgroundColor: 'rgba(3, 72, 243, 0.1)' } |
|
|
|
}, |
|
|
|
// 根据设备类型返回图标 |
|
|
|
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: '18px' }, |
|
|
|
'RFID手持盘点终端': { fontSize: '16px' }, |
|
|
|
'网络高清球机': { fontSize: '16px' }, |
|
|
|
'智能一体机': { fontSize: '14px' }, |
|
|
|
'人脸识别门禁一体机': { fontSize: '14px' } |
|
|
|
} |
|
|
|
return styleMap[typeName] || { fontSize: '12px' } |
|
|
|
}, |
|
|
|
|
|
|
|
getWarehouseTree() { |
|
|
|
FetchWarehouseTree().then(res => { |
|
|
|
this.storeData = res || [] |
|
|
|
// 测试使用 |
|
|
|
// this.storeData = [] |
|
|
|
this.storeData[0].lastSyncTime = '2026-04-03T08:33:19.014+00:00' |
|
|
|
this.storeData[0].syncExplanation = '报警文案报警文案报警文案报警文案报警文案报警文案报警文案报警文案报警文案报警文案' |
|
|
|
|
|
|
|
// this.storeData[2].lastSyncTime = '2026-04-03T08:33:19.014+00:00' |
|
|
|
// this.storeData[2].syncExplanation = 'ddd' |
|
|
|
// console.log('storeData', this.storeData) |
|
|
|
}).catch(() => {}) |
|
|
|
[CRUD.HOOK.afterRefresh]() { |
|
|
|
const apiList = this.crud.data || [] |
|
|
|
this.buildWarehouseCardList(apiList) |
|
|
|
}, |
|
|
|
// 获取当前节点下所有仓库 → 生成卡片(无论有没有设备都显示) |
|
|
|
buildWarehouseCardList(deviceList) { |
|
|
|
// 1. 没有仓库数据直接清空 |
|
|
|
if (!this.storeData.length) { |
|
|
|
buildWarehouseCardList(apiResult) { |
|
|
|
// ========== 测试:打开下面注释,使用mock数据,注释掉原本 apiList ========== |
|
|
|
// const apiList = [ |
|
|
|
// { |
|
|
|
// warehouse: { |
|
|
|
// id: 1, |
|
|
|
// warehouseNo: 'S00101', |
|
|
|
// warehouseName: '中心仓库', |
|
|
|
// warehouseArea: '1200', |
|
|
|
// administrator: '张管理员', |
|
|
|
// contactPhone: '13800138000', |
|
|
|
// fondsNo: 'S001', |
|
|
|
// lastTime: '2026-08-07 10:20:30' |
|
|
|
// }, |
|
|
|
// todayInCount: 0, |
|
|
|
// monthlyInCount: 11, |
|
|
|
// monthlyInboundCount: 99, |
|
|
|
// monthlyOutboundCount: 0, |
|
|
|
// fluctuationCount: 0, |
|
|
|
// fluctuationImportantCount: 0, |
|
|
|
// warnList: [] |
|
|
|
// }, |
|
|
|
// { |
|
|
|
// warehouse: { |
|
|
|
// id: 2, |
|
|
|
// warehouseNo: 'S00201', |
|
|
|
// warehouseName: '襄阳仓库', |
|
|
|
// warehouseArea: '800', |
|
|
|
// administrator: '李管理员', |
|
|
|
// contactPhone: '13900139000', |
|
|
|
// fondsNo: 'S002', |
|
|
|
// lastTime: '2026-08-07 09:10:20' |
|
|
|
// }, |
|
|
|
// todayInCount: 0, |
|
|
|
// monthlyInCount: 0, |
|
|
|
// monthlyInboundCount: 0, |
|
|
|
// monthlyOutboundCount: 0, |
|
|
|
// fluctuationCount: 0, |
|
|
|
// fluctuationImportantCount: 0, |
|
|
|
// warnList: [ |
|
|
|
// { warnValue: '标签异常,资产A001脱离监控标签异常' }, |
|
|
|
// { warnValue: '11111' } |
|
|
|
// ] |
|
|
|
// } |
|
|
|
// ] |
|
|
|
// ========== 测试mock结束 ========== |
|
|
|
|
|
|
|
const apiList = this.crud.data || [] |
|
|
|
|
|
|
|
if (!Array.isArray(apiList) || apiList.length === 0) { |
|
|
|
this.warehouseCardList = [] |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 2. 拿到当前选中的市州节点 |
|
|
|
const currentFondsNo = this.selectedTreeKey?.fondsNo |
|
|
|
|
|
|
|
// 3. 筛选出【当前选中市州下的所有仓库】 |
|
|
|
let allWarehousesInCurrentFonds = [] |
|
|
|
if (this.selectedTreeKey.id === 0) { |
|
|
|
// 选中根节点“市州” → 显示所有仓库 |
|
|
|
allWarehousesInCurrentFonds = this.storeData |
|
|
|
let filterList = [] |
|
|
|
if (this.selectedTreeKey?.id === 0) { |
|
|
|
filterList = apiList |
|
|
|
} else { |
|
|
|
// 选中具体市州 → 只显示该市州下的仓库 |
|
|
|
allWarehousesInCurrentFonds = this.storeData.filter( |
|
|
|
s => s.fondsNo === currentFondsNo |
|
|
|
) |
|
|
|
const selectFondsNo = this.selectedTreeKey?.fondsNo |
|
|
|
filterList = apiList.filter(item => item.warehouse.fondsNo === selectFondsNo) |
|
|
|
} |
|
|
|
|
|
|
|
// 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) |
|
|
|
} |
|
|
|
const cardList = filterList.map(item => { |
|
|
|
return { |
|
|
|
...item, |
|
|
|
isWarningActive: Array.isArray(item.warnList) && item.warnList.length > 0 |
|
|
|
} |
|
|
|
|
|
|
|
cardList.push({ |
|
|
|
...warehouse, |
|
|
|
deviceList: deviceListOfWarehouse, |
|
|
|
isWarningActive, |
|
|
|
warningText, |
|
|
|
warningNeedScroll: false |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
this.warehouseCardList = cardList |
|
|
|
|
|
|
|
// 6. 启动报警滚动 |
|
|
|
console.log('cardList', cardList) |
|
|
|
this.$nextTick(() => { |
|
|
|
this.startAllTextScroll() |
|
|
|
}) |
|
|
|
}, |
|
|
|
|
|
|
|
// 保存多个警告文字DOM |
|
|
|
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.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) |
|
|
|
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) // 每条停留3s |
|
|
|
} |
|
|
|
|
|
|
|
const stop = () => { |
|
|
|
clearTimeout(timer) |
|
|
|
} |
|
|
|
this.warningScrollTasks.push({ stop }) |
|
|
|
scrollStep() |
|
|
|
}) |
|
|
|
}, |
|
|
|
|
|
|
|
// 过滤树数据 |
|
|
|
filterTreeData(tree) { |
|
|
|
return tree.map(node => { |
|
|
|
if (node.type === 'warehouse') return null |
|
|
|
@ -397,28 +346,15 @@ export default { |
|
|
|
}).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) { |
|
|
|
@ -431,89 +367,114 @@ export default { |
|
|
|
this.crud.toQuery() |
|
|
|
}, |
|
|
|
|
|
|
|
// 关闭弹窗 |
|
|
|
handleClose() { |
|
|
|
this.hkVideoVisible = false |
|
|
|
}, |
|
|
|
openWarehouseCamera(warehouse) { |
|
|
|
// warehouse.deviceList 为当前仓库所有设备数组,根据你的业务筛选摄像头 |
|
|
|
const cameraList = warehouse.deviceList || [] |
|
|
|
// 筛选监控球机,按需修改判断条件 |
|
|
|
const camera = cameraList.find(item => item.deviceTypeName === '网络高清球机') |
|
|
|
|
|
|
|
if (!camera) { |
|
|
|
this.$message.warning('该仓库暂无监控设备') |
|
|
|
return |
|
|
|
|
|
|
|
// async openWarehouseCamera(cardItem) { |
|
|
|
// const warehouseNo = cardItem.warehouseNo |
|
|
|
// try { |
|
|
|
// // 接口返回全部仓库的设备(你这里没有传参) |
|
|
|
// const res = await FetchDeviceAllByWarehouseId() |
|
|
|
// const allDevices = res || [] |
|
|
|
|
|
|
|
// // 1.先过滤设备类型为网络高清球机 |
|
|
|
// // 2.再过滤:球机的warehouseNo === 当前点击卡片的warehouseNo |
|
|
|
// const ballDevices = allDevices.filter(d => { |
|
|
|
// return d.deviceTypeName === '网络高清球机' && d.warehouseNo === warehouseNo |
|
|
|
// }) |
|
|
|
|
|
|
|
// if (ballDevices.length === 0) { |
|
|
|
// this.$message.info('该仓库暂无网络高清球机设备') |
|
|
|
// return |
|
|
|
// } |
|
|
|
// console.log('当前仓库匹配的球机', ballDevices) |
|
|
|
// // 取该仓库下第一个球机的id |
|
|
|
// this.handleViewVideo(ballDevices[0].id) |
|
|
|
// } catch (err) { |
|
|
|
// console.error('获取仓库设备异常:', err) |
|
|
|
// this.$message.error('获取仓库设备失败') |
|
|
|
// } |
|
|
|
// }, |
|
|
|
|
|
|
|
async openWarehouseCamera(cardItem) { |
|
|
|
const warehouseId = cardItem.id |
|
|
|
try { |
|
|
|
// ✅ 传给接口 warehouseId,后端直接返回该仓库下所有设备 |
|
|
|
const res = await FetchDeviceAllByWarehouseId({ warehouseId }) |
|
|
|
const allDevices = res || [] |
|
|
|
|
|
|
|
// 此时 allDevices 已经仅是当前仓库的设备,只需要过滤设备类型 |
|
|
|
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('获取仓库设备失败') |
|
|
|
} |
|
|
|
// 直接传入设备ID,调用你原有不变的handleViewVideo |
|
|
|
this.handleViewVideo(camera.id) |
|
|
|
}, |
|
|
|
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() |
|
|
|
}) |
|
|
|
|
|
|
|
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 |
|
|
|
} |
|
|
|
}).catch(err => { |
|
|
|
console.log('获取设备视频信息失败', err) |
|
|
|
}) |
|
|
|
this.hkVideoVisible = true |
|
|
|
this.$nextTick(() => { |
|
|
|
this.$refs.hkVideoRef.initVideo() |
|
|
|
}) |
|
|
|
} catch (err) { |
|
|
|
console.error('打开视频失败:', err) |
|
|
|
this.$message.error('获取监控设备信息失败') |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
::v-deep .el-tree{ |
|
|
|
.el-tree-node__content{ |
|
|
|
::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; |
|
|
|
} |
|
|
|
.el-tree-node__children { |
|
|
|
.el-tree-node__content { |
|
|
|
font-size: 14px; |
|
|
|
color: #545B65; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.monitor-list{ |
|
|
|
.monitor-list { |
|
|
|
display: flex; |
|
|
|
justify-content: flex-start; |
|
|
|
align-items: flex-start; |
|
|
|
flex-wrap: wrap; |
|
|
|
padding-top: 15px; |
|
|
|
.list-card{ |
|
|
|
.list-card { |
|
|
|
width: calc(100% / 3 - 20px); |
|
|
|
margin: 0 10px 20px 10px; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-card { |
|
|
|
border-radius: 10px; |
|
|
|
.el-card__header{ |
|
|
|
.el-card__header { |
|
|
|
height: 66px; |
|
|
|
padding: 12px 10px !important; |
|
|
|
border-radius: 10px 10px 0 0; |
|
|
|
@ -522,30 +483,27 @@ export default { |
|
|
|
font-weight: 600; |
|
|
|
background-color: #fff !important; |
|
|
|
border-bottom: 1px solid #ebeef5 !important; |
|
|
|
.store-info{ |
|
|
|
.store-info { |
|
|
|
display: flex; |
|
|
|
justify-content: flex-start; |
|
|
|
align-items: center; |
|
|
|
.store-info-item{ |
|
|
|
.store-info-item { |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
p{ |
|
|
|
p { |
|
|
|
margin-right: 6px; |
|
|
|
} |
|
|
|
&.store-top{ |
|
|
|
&.store-top { |
|
|
|
justify-content: space-between; |
|
|
|
.iconfont::before{ |
|
|
|
margin: 0 !important; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
.el-card__body{ |
|
|
|
.el-card__body { |
|
|
|
padding: 5px 10px; |
|
|
|
border-radius: 0 0 10px 10px; |
|
|
|
.store-monitor{ |
|
|
|
.store-monitor { |
|
|
|
display: flex; |
|
|
|
flex-wrap: wrap; |
|
|
|
justify-content: flex-start; |
|
|
|
@ -553,119 +511,78 @@ export default { |
|
|
|
padding: 5px 5px 10px 5px; |
|
|
|
line-height: 35px; |
|
|
|
font-size: 14px; |
|
|
|
li{ |
|
|
|
li { |
|
|
|
width: calc(50% - 10px); |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
&:nth-child(even){ |
|
|
|
&:nth-child(even) { |
|
|
|
margin-left: 15px; |
|
|
|
} |
|
|
|
span{ |
|
|
|
span { |
|
|
|
display: block; |
|
|
|
width: 120px; |
|
|
|
// color: #0348f3; |
|
|
|
} |
|
|
|
p{ |
|
|
|
p { |
|
|
|
flex: 1; |
|
|
|
text-align: right; |
|
|
|
font-weight: bold; |
|
|
|
color: #0C0E1E; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
.warning-text{ |
|
|
|
.warning-text { |
|
|
|
color: #F65163; |
|
|
|
font-size: 14px; |
|
|
|
font-weight: 600; |
|
|
|
border-top: 1px solid #ebeef5 !important; |
|
|
|
border-top: 1px solid #ebeef5; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 6px; |
|
|
|
height: 44px; |
|
|
|
overflow: hidden; |
|
|
|
padding: 0 10px !important; |
|
|
|
padding: 0 10px; |
|
|
|
|
|
|
|
.iconfont{ |
|
|
|
.iconfont { |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.warning-text-scroll { |
|
|
|
flex: 1; |
|
|
|
height: 100%; |
|
|
|
height: 44px; |
|
|
|
overflow: hidden; |
|
|
|
line-height: 44px; |
|
|
|
position: relative; |
|
|
|
} |
|
|
|
|
|
|
|
.scroll-text { |
|
|
|
display: inline-block; |
|
|
|
white-space: nowrap; |
|
|
|
.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{ |
|
|
|
&.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; |
|
|
|
.el-card__body { |
|
|
|
background-color: rgba(85, 85, 85, 0.2) !important; |
|
|
|
} |
|
|
|
.warning-text{ |
|
|
|
border-top: 1px solid #fff !important; |
|
|
|
.warning-text { |
|
|
|
border-top: 1px solid #fff; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
.view-video{ |
|
|
|
::v-deep .el-dialog{ |
|
|
|
width: 1000px !important; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.swiper-content{ |
|
|
|
height: 210px; |
|
|
|
.device-grid { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: repeat(1, 1fr); // 3列 |
|
|
|
grid-template-rows: repeat(6, 30px); // 2行 |
|
|
|
gap: 4px; |
|
|
|
height: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.device-item { |
|
|
|
display: flex; |
|
|
|
justify-content: flex-start; |
|
|
|
align-items: center; |
|
|
|
// border-radius: 4px; |
|
|
|
padding: 0 6px; |
|
|
|
p{ |
|
|
|
flex: 1; |
|
|
|
font-size: 14px; |
|
|
|
margin-left: 10px; |
|
|
|
text-align: left; |
|
|
|
} |
|
|
|
&:hover{ |
|
|
|
background-color: rgba(245, 249, 252, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.swiper-slide-content{ |
|
|
|
height: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.iconfont::before{ |
|
|
|
margin-right: 0 !important; |
|
|
|
.view-video { |
|
|
|
::v-deep .el-dialog { |
|
|
|
width: 1000px !important; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 分页指示器样式 |
|
|
|
::v-deep .swiper-pagination { |
|
|
|
bottom: 0 !important; |
|
|
|
} |
|
|
|
</style> |