无线电资产管理
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

5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
1 month ago
5 months ago
1 month ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
5 months ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
1 month ago
5 months ago
1 month ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
1 month ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
1 month ago
5 months ago
  1. <template>
  2. <div class="app-container">
  3. <div class="container-main" style="justify-content: flex-start;">
  4. <div class="elect-cont-left">
  5. <el-scrollbar style="height: calc(100vh - 198px);">
  6. <el-tree
  7. ref="tree"
  8. :data="warehouseTreeData"
  9. :props="defaultProps"
  10. node-key="id"
  11. default-expand-all
  12. :default-checked-keys="defaultCheckedKeys"
  13. :expand-on-click-node="false"
  14. :highlight-current="true"
  15. @node-click="handleNodeClick"
  16. />
  17. </el-scrollbar>
  18. </div>
  19. <div class="elect-cont-right" style="background-color: transparent; padding: 0 !important;">
  20. <div class="container-right" style="height: calc(100vh - 158px) !important;background-color: transparent !important;">
  21. <span class="right-top-line" />
  22. <span class="left-bottom-line" />
  23. <div class="monitor-list" style="height: 100%; overflow-y: auto;">
  24. <el-card
  25. v-for="(item, index) in warehouseCardList"
  26. :key="index"
  27. class="list-card"
  28. :class="{ 'warning-active': item.isWarningActive }"
  29. >
  30. <div slot="header" class="store-info">
  31. <img src="@/assets/images/tab_archives_logo.png" alt="" style="display: block; width: 50px; margin-right: 10px;">
  32. <div style="flex: 1;">
  33. <div class="store-info-item store-top">
  34. <p style="width:62%;">{{ item.warehouse.warehouseName || '仓库' }}</p>
  35. <p style="flex:1;">面积{{ item.warehouse.warehouseArea || '0' }} </p>
  36. <i
  37. style="width:6%; text-align: center; cursor: pointer;"
  38. class="iconfont icon-wangluoshexiangtou"
  39. @click.stop="openWarehouseCamera(item.warehouse)"
  40. />
  41. </div>
  42. <div class="store-info-item" style="font-size: 12px; color: #909399; margin-top: 10px;">
  43. <div style="display: flex; justify-content: flex-start;">
  44. <p v-if="item.warehouse.administrator">{{ item.warehouse.administrator }}</p>
  45. <p v-if="item.warehouse.contactPhone">&nbsp;{{ item.warehouse.contactPhone }}</p>
  46. </div>
  47. <p v-if="item.warehouse.lastTime">最后同步时间: {{ item.warehouse.lastTime | parseTime }}</p>
  48. </div>
  49. </div>
  50. </div>
  51. <ul class="store-monitor">
  52. <li>
  53. <span>当日进出</span>
  54. <p>{{ item.todayInCount || 0 }}</p>
  55. </li>
  56. <li>
  57. <span>本月进出</span>
  58. <p>{{ item.monthlyInCount || 0 }}</p>
  59. </li>
  60. <li>
  61. <span>本月入库</span>
  62. <p>{{ item.monthlyInboundCount || 0 }}</p>
  63. </li>
  64. <li>
  65. <span>本月出库</span>
  66. <p>{{ item.monthlyOutboundCount || 0 }}</p>
  67. </li>
  68. <li>
  69. <span>本月出入库异动</span>
  70. <p>{{ item.fluctuationCount || 0 }}</p>
  71. </li>
  72. <li>
  73. <span>本月重点资产异动</span>
  74. <p>{{ item.fluctuationImportantCount || 0 }}</p>
  75. </li>
  76. </ul>
  77. <div class="warning-text">
  78. <i class="iconfont icon-baojingjilu" />
  79. <div v-if="item.isWarningActive" :ref="el => setWarningRef(el, index)" class="warning-text-scroll">
  80. <div class="scroll-wrap">
  81. <div v-for="(warn, warnIdx) in item.warnList" :key="warnIdx" class="scroll-item">
  82. {{ warn.warnValue }}
  83. </div>
  84. <!-- 复制一份用于无缝循环 -->
  85. <div v-for="(warn, warnIdx) in item.warnList" :key="'copy_'+warnIdx" class="scroll-item">
  86. {{ warn.warnValue }}
  87. </div>
  88. </div>
  89. </div>
  90. <div v-else style="font-size: 12px; color: #999; flex: 1;">暂无报警信息</div>
  91. <i class="el-icon-more" style="font-size: 12px; cursor: pointer;" @click.stop="handleMoreClick(item)" />
  92. </div>
  93. </el-card>
  94. <div v-if="warehouseCardList.length === 0 && !crud.loading" class="no-device">
  95. <el-empty style="padding:80px 0;" :image-size="60" description="暂无仓库信息" />
  96. </div>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. <!-- 监控视频弹窗 -->
  102. <el-dialog
  103. class="view-video"
  104. append-to-body
  105. :close-on-click-modal="false"
  106. :modal-append-to-body="false"
  107. :visible="hkVideoVisible"
  108. title="查看监控视频"
  109. :before-close="handleClose"
  110. >
  111. <span class="dialog-right-top" />
  112. <span class="dialog-left-bottom" />
  113. <div class="setting-dialog">
  114. <hkVideo ref="hkVideoRef" :hk-config="hkConfig" />
  115. </div>
  116. </el-dialog>
  117. <!-- ==========新增历史报警弹窗========== -->
  118. <el-dialog
  119. title="历史报警信息"
  120. append-to-body
  121. width="900px"
  122. :visible.sync="alarmDialogVisible"
  123. >
  124. <el-table
  125. ref="table"
  126. class="archives-table"
  127. stripe
  128. style="width: 100%;"
  129. height="calc(100vh - 530px)"
  130. :data="alarmTableData"
  131. >
  132. <el-table-column prop="warehouseName" label="仓库名称" />
  133. <el-table-column prop="warnValue" label="报警信息" />
  134. <el-table-column prop="startTime" label="报警时间" width="160">
  135. <template slot-scope="scope">
  136. {{ scope.row.startTime | parseTime }}
  137. </template>
  138. </el-table-column>
  139. </el-table>
  140. <el-pagination
  141. style="margin-top:16px;text-align:right;"
  142. :current-page="alarmPageNum"
  143. :page-size="alarmPageSize"
  144. :total="alarmTotal"
  145. layout="total, prev, pager, next"
  146. @current-change="alarmPageChange"
  147. />
  148. </el-dialog>
  149. </div>
  150. </template>
  151. <script>
  152. import crudDevice, { FetchFondsWarehouseTree, FetchDeviceAllByWarehouseId, FetchInitAlarmsLog } from '@/api/assetDevice/index'
  153. import CRUD, { presenter, header, crud } from '@crud/crud'
  154. import hkVideo from '@/views/components/hkVideo.vue'
  155. export default {
  156. name: 'Monitor',
  157. components: {
  158. hkVideo
  159. },
  160. cruds() {
  161. return CRUD({
  162. title: '站点监控',
  163. url: 'api/warehouse/warehouseMonitoring',
  164. idField: 'warehouse.id',
  165. sort: [],
  166. crudMethod: { ...crudDevice },
  167. optShow: {
  168. add: false,
  169. edit: false,
  170. del: false,
  171. reset: true,
  172. download: false,
  173. group: false
  174. },
  175. queryOnPresenterCreated: false
  176. })
  177. },
  178. mixins: [presenter(), header(), crud()],
  179. data() {
  180. return {
  181. permission: { add: [], edit: [], del: [] },
  182. warehouseTreeData: null,
  183. defaultProps: {
  184. children: 'children',
  185. label: 'name'
  186. },
  187. defaultCheckedKeys: [],
  188. selectedTreeKey: null,
  189. warehouseCardList: [],
  190. warningRefs: [],
  191. warningIntervals: [],
  192. warningScrollTasks: [],
  193. hkVideoVisible: false,
  194. hkConfig: {
  195. username: null,
  196. password: null,
  197. ip: null,
  198. port: null
  199. },
  200. // ==========历史报警弹窗新增data==========
  201. alarmDialogVisible: false,
  202. alarmLoading: false,
  203. alarmWarehouseNo: '',
  204. alarmTableData: [],
  205. alarmPageNum: 1,
  206. alarmPageSize: 10,
  207. alarmTotal: 0
  208. }
  209. },
  210. created() {
  211. FetchFondsWarehouseTree().then(res => {
  212. const filteredData = this.filterTreeData(res)
  213. const topParent = {
  214. id: 0,
  215. name: '市州',
  216. children: filteredData
  217. }
  218. this.warehouseTreeData = [topParent]
  219. this.$nextTick(() => {
  220. this.selectFirstWarehouseNode()
  221. })
  222. }).catch(() => {})
  223. },
  224. beforeDestroy() {
  225. this.clearAllWarningScroll()
  226. },
  227. methods: {
  228. [CRUD.HOOK.beforeRefresh]() {
  229. this.crud.params.page = null
  230. this.crud.params.size = null
  231. },
  232. [CRUD.HOOK.afterRefresh]() {
  233. const apiList = this.crud.data || []
  234. this.buildWarehouseCardList(apiList)
  235. },
  236. buildWarehouseCardList(apiResult) {
  237. const apiList = this.crud.data || []
  238. if (!Array.isArray(apiList) || apiList.length === 0) {
  239. this.warehouseCardList = []
  240. return
  241. }
  242. let filterList = []
  243. if (this.selectedTreeKey?.id === 0) {
  244. filterList = apiList
  245. } else {
  246. const selectFondsNo = this.selectedTreeKey?.fondsNo
  247. filterList = apiList.filter(item => item.warehouse.fondsNo === selectFondsNo)
  248. }
  249. const cardList = filterList.map(item => {
  250. return {
  251. ...item,
  252. isWarningActive: Array.isArray(item.warnList) && item.warnList.length > 0
  253. }
  254. })
  255. this.warehouseCardList = cardList
  256. console.log('cardList', cardList)
  257. this.$nextTick(() => {
  258. this.startAllTextScroll()
  259. })
  260. },
  261. setWarningRef(el, index) {
  262. if (el) this.warningRefs[index] = el
  263. },
  264. clearAllWarningScroll() {
  265. this.warningScrollTasks.forEach(task => {
  266. if (task && typeof task.stop === 'function') task.stop()
  267. })
  268. this.warningScrollTasks = []
  269. },
  270. startAllTextScroll() {
  271. this.clearAllWarningScroll()
  272. this.warningRefs.forEach(el => {
  273. if (!el) return
  274. const wrapDom = el.querySelector('.scroll-wrap')
  275. if (wrapDom) {
  276. wrapDom.style.transform = `translateY(0px)`
  277. wrapDom.style.transition = ''
  278. }
  279. })
  280. this.warehouseCardList.forEach((card, idx) => {
  281. if (!card.isWarningActive || !card.warnList || card.warnList.length <= 0) return
  282. const boxDom = this.warningRefs[idx]
  283. if (!boxDom) return
  284. const wrapDom = boxDom.querySelector('.scroll-wrap')
  285. if (!wrapDom) return
  286. const itemHeight = 44
  287. const originLen = card.warnList.length
  288. const totalOriginHeight = originLen * itemHeight
  289. let offset = 0
  290. let timer = null
  291. const scrollStep = () => {
  292. wrapDom.style.transition = 'transform 0.6s ease-in-out'
  293. offset += itemHeight
  294. wrapDom.style.transform = `translateY(-${offset}px)`
  295. timer = setTimeout(() => {
  296. if (offset >= totalOriginHeight) {
  297. wrapDom.style.transition = 'none'
  298. offset = 0
  299. wrapDom.style.transform = `translateY(0px)`
  300. wrapDom.offsetHeight
  301. }
  302. scrollStep()
  303. }, 3000)
  304. }
  305. const stop = () => {
  306. clearTimeout(timer)
  307. }
  308. this.warningScrollTasks.push({ stop })
  309. scrollStep()
  310. })
  311. },
  312. filterTreeData(tree) {
  313. return tree.map(node => {
  314. if (node.type === 'warehouse') return null
  315. const newNode = { ...node }
  316. if (newNode.children && newNode.children.length) {
  317. newNode.children = this.filterTreeData(newNode.children).filter(item => item)
  318. }
  319. return newNode
  320. }).filter(item => item)
  321. },
  322. selectFirstWarehouseNode() {
  323. const root = this.warehouseTreeData[0]
  324. if (root) {
  325. this.defaultCheckedKeys = [root.id]
  326. this.$refs.tree.setCurrentKey(root.id)
  327. this.handleNodeClick(root)
  328. }
  329. },
  330. handleNodeClick(data) {
  331. this.selectedTreeKey = data
  332. if (this.selectedTreeKey.id === 0) {
  333. this.crud.query.warehouseId = null
  334. this.crud.query.fondsId = null
  335. } else {
  336. this.crud.query.fondsId = data.id
  337. }
  338. this.search = ''
  339. this.crud.toQuery()
  340. },
  341. handleClose() {
  342. this.hkVideoVisible = false
  343. },
  344. async openWarehouseCamera(cardItem) {
  345. const warehouseId = cardItem.id
  346. try {
  347. const res = await FetchDeviceAllByWarehouseId({ warehouseId })
  348. const allDevices = res || []
  349. const ballDevices = allDevices.filter(d => d.deviceTypeName === '网络高清球机')
  350. if (ballDevices.length === 0) {
  351. this.$message.info('该仓库暂无网络高清球机设备')
  352. return
  353. }
  354. console.log('当前仓库匹配的球机', ballDevices)
  355. this.handleViewVideo(ballDevices[0].id)
  356. } catch (err) {
  357. console.error('获取仓库设备异常:', err)
  358. this.$message.error('获取仓库设备失败')
  359. }
  360. },
  361. async handleViewVideo(deviceId) {
  362. try {
  363. const [detailRes, pwdRes] = await Promise.all([
  364. crudDevice.FetchDeviceDetailsById({ deviceId }),
  365. crudDevice.FetchDevicePasswordById({ deviceId })
  366. ])
  367. if (!detailRes) return
  368. const finalPassword = pwdRes || detailRes.devicePassword
  369. this.hkConfig = {
  370. username: detailRes.deviceAccount,
  371. password: finalPassword,
  372. ip: detailRes.deviceIp,
  373. port: detailRes.devicePort
  374. }
  375. this.hkVideoVisible = true
  376. this.$nextTick(() => {
  377. this.$refs.hkVideoRef.initVideo()
  378. })
  379. } catch (err) {
  380. console.error('打开视频失败:', err)
  381. this.$message.error('获取监控设备信息失败')
  382. }
  383. },
  384. // 修改后 handleMoreClick
  385. handleMoreClick(item) {
  386. this.alarmWarehouseNo = item.warehouse.warehouseNo
  387. this.alarmPageNum = 1
  388. this.alarmDialogVisible = true
  389. this.getAlarmList()
  390. },
  391. // 获取报警列表
  392. async getAlarmList() {
  393. this.alarmLoading = true
  394. try {
  395. const param = {
  396. warehouseNo: this.alarmWarehouseNo,
  397. pageNum: this.alarmPageNum,
  398. pageSize: this.alarmPageSize
  399. }
  400. const res = await FetchInitAlarmsLog(param)
  401. this.alarmTableData = res.records || []
  402. this.alarmTotal = res.total || 0
  403. } catch (e) {
  404. this.$message.error('查询历史报警失败')
  405. } finally {
  406. this.alarmLoading = false
  407. }
  408. },
  409. // 分页切换
  410. alarmPageChange(page) {
  411. this.alarmPageNum = page
  412. this.getAlarmList()
  413. }
  414. }
  415. }
  416. </script>
  417. <style lang="scss" scoped>
  418. ::v-deep .el-tree {
  419. .el-tree-node__content {
  420. font-size: 14px;
  421. color: #545B65;
  422. }
  423. .el-tree-node__children {
  424. .el-tree-node__content {
  425. font-size: 14px;
  426. color: #545B65;
  427. }
  428. }
  429. }
  430. .monitor-list {
  431. display: flex;
  432. justify-content: flex-start;
  433. align-items: flex-start;
  434. flex-wrap: wrap;
  435. padding-top: 15px;
  436. .list-card {
  437. width: calc(100% / 3 - 20px);
  438. margin: 0 10px 20px 10px;
  439. }
  440. }
  441. ::v-deep .el-card {
  442. border-radius: 10px;
  443. .el-card__header {
  444. height: 66px;
  445. padding: 12px 10px !important;
  446. border-radius: 10px 10px 0 0;
  447. color: #0c0e1e;
  448. font-size: 16px;
  449. font-weight: 600;
  450. background-color: #fff !important;
  451. border-bottom: 1px solid #ebeef5 !important;
  452. .store-info {
  453. display: flex;
  454. justify-content: flex-start;
  455. align-items: center;
  456. .store-info-item {
  457. display: flex;
  458. justify-content: space-between;
  459. align-items: center;
  460. p {
  461. margin-right: 6px;
  462. }
  463. &.store-top {
  464. justify-content: space-between;
  465. }
  466. }
  467. }
  468. }
  469. .el-card__body {
  470. padding: 5px 10px;
  471. border-radius: 0 0 10px 10px;
  472. .store-monitor {
  473. display: flex;
  474. flex-wrap: wrap;
  475. justify-content: flex-start;
  476. align-items: flex-start;
  477. padding: 5px 5px 10px 5px;
  478. line-height: 35px;
  479. font-size: 14px;
  480. li {
  481. width: calc(50% - 10px);
  482. display: flex;
  483. justify-content: space-between;
  484. align-items: center;
  485. &:nth-child(even) {
  486. margin-left: 15px;
  487. }
  488. span {
  489. display: block;
  490. width: 120px;
  491. }
  492. p {
  493. flex: 1;
  494. text-align: right;
  495. font-weight: bold;
  496. color: #0C0E1E;
  497. }
  498. }
  499. }
  500. }
  501. .warning-text {
  502. font-size: 14px;
  503. font-weight: 600;
  504. border-top: 1px solid #ebeef5;
  505. display: flex;
  506. align-items: center;
  507. gap: 6px;
  508. height: 44px;
  509. overflow: hidden;
  510. padding: 0 10px;
  511. .iconfont {
  512. flex-shrink: 0;
  513. color: #F65163;
  514. }
  515. .warning-text-scroll {
  516. flex: 1;
  517. height: 44px;
  518. overflow: hidden;
  519. position: relative;
  520. }
  521. .scroll-wrap {
  522. position: absolute;
  523. left: 0;
  524. top: 0;
  525. width: 100%;
  526. }
  527. .scroll-item {
  528. height: 44px;
  529. line-height: 44px;
  530. white-space: nowrap;
  531. }
  532. }
  533. &.warning-active {
  534. .el-card__header {
  535. background-color: rgba(85, 85, 85, 0.2) !important;
  536. border-bottom: 1px solid #fff !important;
  537. }
  538. .el-card__body {
  539. background-color: rgba(85, 85, 85, 0.2) !important;
  540. }
  541. .warning-text {
  542. border-top: 1px solid #fff;
  543. }
  544. }
  545. }
  546. .view-video {
  547. ::v-deep .el-dialog {
  548. width: 1000px !important;
  549. }
  550. }
  551. </style>