index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <template>
  2. <div class="safetyList">
  3. <customHeader>数据中心-分站监测</customHeader>
  4. <div class="content">
  5. <a-tabs class="tab-box" v-model:activeKey="activeKey" @change="onChangeTab">
  6. <a-tab-pane tab="分站监测" key="device" />
  7. </a-tabs>
  8. <div class="box-content">
  9. <!-- 分站监测 -->
  10. <div class="now-content">
  11. <div class="left-box">
  12. <div class="left-content">
  13. <div class="card-box" v-for="(item, index) in cardList" :key="index">
  14. <div :class="activeIndex1 === index ? 'card-itemD' : 'card-itemL'" @click="cardClick(item, index)">
  15. <div class="card-item-label">{{ item.strname }}</div>
  16. </div>
  17. </div>
  18. </div>
  19. </div>
  20. <div class="right-box">
  21. <div class="right-title">详细信息:</div>
  22. <a-table
  23. size="small"
  24. :scroll="{ y: 650 }"
  25. :columns="outerColumns"
  26. :data-source="deviceList"
  27. :pagination="false"
  28. :row-key="(record) => record.id"
  29. :expand-row-by-click="true"
  30. :expandedRowKeys="expandedRowKeys"
  31. @expand="handleExpand"
  32. >
  33. <!-- 自定义展开图标 -->
  34. <template #expandIcon="{ expanded, onExpand, record }">
  35. <a-button
  36. type="text"
  37. size="small"
  38. @click="
  39. (e) => {
  40. e.stopPropagation();
  41. toggleExpand(record.id);
  42. }
  43. "
  44. >
  45. <DownCircleTwoTone v-if="expandedRowKeys.includes(record.id)" />
  46. <RightCircleTwoTone v-else />
  47. </a-button>
  48. </template>
  49. <!-- 嵌套表格 -->
  50. <template #expandedRowRender="{ record }">
  51. <a-table
  52. size="small"
  53. :columns="innerColumns"
  54. :data-source="monitorList"
  55. :pagination="false"
  56. :loading="loadingMap[record.id]"
  57. bordered
  58. :scroll="{ y: 410 }"
  59. >
  60. <template #bodyCell="{ column, record: innerRecord }">
  61. <template v-if="column.dataIndex === 'value'">
  62. <span>
  63. {{ innerRecord.value }}
  64. </span>
  65. </template>
  66. </template>
  67. </a-table>
  68. </template>
  69. </a-table>
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. </template>
  76. <script setup lang="ts">
  77. import { ref, nextTick, computed, reactive, onMounted, onUnmounted, inject } from 'vue';
  78. import { usePermission } from '/@/hooks/web/usePermission';
  79. import customHeader from '/@/components/vent/customHeader.vue';
  80. import { message, TreeProps } from 'ant-design-vue';
  81. import { getDeviceList, getDeviceListByType, getDevMonitorListById } from './device.api';
  82. import { subStationList } from './device.api';
  83. import { RightCircleTwoTone, DownCircleTwoTone } from '@ant-design/icons-vue';
  84. import Index from '/@/layouts/page/index.vue';
  85. const { hasPermission } = usePermission();
  86. let activeKey = ref('device');
  87. //当前左侧激活菜单的索引
  88. let activeIndex1 = ref(0);
  89. const cardList = ref<any[]>(); //分站列表
  90. const deviceList = ref<any[]>([]);
  91. const openNum = ref(0);
  92. const clsoeNum = ref(0);
  93. const monitorList = ref<any[]>([]); // 监测数据列表
  94. const expandedRowKeys = ref([]);
  95. // // 分页参数
  96. // const paginationState = ref({
  97. // current: 1,
  98. // pageSize: 10,
  99. // total: 0,
  100. // });
  101. // const paginationState2 = ref({
  102. // current: 1,
  103. // pageSize: 10,
  104. // total: 0,
  105. // });
  106. // // 计算分页后的数据
  107. // const paginatedData = computed(() => {
  108. // const start = (paginationState.value.current - 1) * paginationState.value.pageSize;
  109. // const end = start + paginationState.value.pageSize;
  110. // return monitorList.value.slice(start, end);
  111. // });
  112. // // 计算分页后的数据
  113. // const paginatedData2 = computed(() => {
  114. // const start = (paginationState2.value.current - 1) * paginationState2.value.pageSize;
  115. // const end = start + paginationState2.value.pageSize;
  116. // return deviceList.value.slice(start, end);
  117. // });
  118. // // 分页器配置 - 修复响应式问题
  119. // const paginationConfig = computed(() => {
  120. // return {
  121. // current: paginationState.value.current,
  122. // pageSize: paginationState.value.pageSize,
  123. // total: monitorList.value.length,
  124. // showSizeChanger: true,
  125. // showQuickJumper: true,
  126. // showTotal: (total) => `共 ${total} 条`,
  127. // pageSizeOptions: ['10', '20', '50', '100'],
  128. // size: 'small',
  129. // onChange: (page, pageSize) => {
  130. // paginationState.value.current = page;
  131. // paginationState.value.pageSize = pageSize;
  132. // },
  133. // onShowSizeChange: (current, size) => {
  134. // paginationState.value.current = 1;
  135. // paginationState.value.pageSize = size;
  136. // },
  137. // };
  138. // });
  139. // const paginationConfig2 = computed(() => {
  140. // return {
  141. // current: paginationState2.value.current,
  142. // pageSize: paginationState2.value.pageSize,
  143. // total: deviceList.value.length,
  144. // showSizeChanger: true,
  145. // showQuickJumper: true,
  146. // showTotal: (total) => `共 ${total} 条`,
  147. // pageSizeOptions: ['10', '20', '50', '100'],
  148. // size: 'small',
  149. // onChange: (page, pageSize) => {
  150. // paginationState2.value.current = page;
  151. // paginationState2.value.pageSize = pageSize;
  152. // },
  153. // onShowSizeChange: (current, size) => {
  154. // paginationState2.value.current = 1;
  155. // paginationState2.value.pageSize = size;
  156. // },
  157. // };
  158. // });
  159. //获取分站信息
  160. async function getSubStationList() {
  161. let res = await subStationList({ pageSize: 1000, pageNo: 1 });
  162. if (res.length != 0) {
  163. cardList.value = [...res];
  164. getDeviceList(cardList.value[0].id);
  165. openNum.value = cardList.value?.filter((v) => v.linkstatus == 1)['length'];
  166. clsoeNum.value = cardList.value?.filter((v) => v.linkstatus == 0)['length'];
  167. } else {
  168. cardList.value = [];
  169. }
  170. }
  171. //菜单选项切换
  172. function cardClick(item, ind) {
  173. if (timer) {
  174. clearInterval(timer);
  175. timer = undefined;
  176. }
  177. activeIndex1.value = ind;
  178. getDeviceList(item.id);
  179. }
  180. // 加载状态映射
  181. const loadingMap = reactive({});
  182. // 根据选择设备获取设备列表
  183. async function getDeviceList(ID?) {
  184. const params: any = {
  185. subId: ID.toString(),
  186. };
  187. const result = await getDeviceListByType(params);
  188. deviceList.value = result.records;
  189. }
  190. // 外层表格列配置
  191. const outerColumns = [
  192. {
  193. title: '设备ID',
  194. dataIndex: 'id',
  195. key: 'id',
  196. align: 'center',
  197. },
  198. {
  199. title: '安装位置',
  200. dataIndex: 'strinstallpos',
  201. key: 'strinstallpos',
  202. align: 'center',
  203. },
  204. {
  205. title: '设备类型',
  206. dataIndex: 'devicekind_dictText',
  207. key: 'devicekind_dictText',
  208. align: 'center',
  209. },
  210. {
  211. title: '状态',
  212. dataIndex: 'netStatus',
  213. key: 'netStatus',
  214. align: 'center',
  215. customRender: ({ text }) => {
  216. text = '在线';
  217. return `${text}`;
  218. },
  219. },
  220. ];
  221. // 内层表格列配置
  222. const innerColumns = [
  223. {
  224. title: '地址',
  225. dataIndex: 'plcAddr',
  226. key: 'plcAddr',
  227. align: 'center',
  228. },
  229. {
  230. title: '数据code',
  231. dataIndex: 'valueCode',
  232. align: 'center',
  233. key: 'valueCode',
  234. },
  235. {
  236. title: '数据名称',
  237. dataIndex: 'valueName',
  238. align: 'center',
  239. key: 'valueName',
  240. },
  241. {
  242. title: '数据值',
  243. dataIndex: 'value',
  244. align: 'center',
  245. key: 'value',
  246. },
  247. {
  248. title: '时间',
  249. dataIndex: 'time',
  250. align: 'center',
  251. key: 'time',
  252. },
  253. ];
  254. // 切换展开状态
  255. const toggleExpand = (id) => {
  256. const index = expandedRowKeys.value.indexOf(id);
  257. if (index > -1) {
  258. expandedRowKeys.value.splice(index, 1);
  259. } else {
  260. expandedRowKeys.value = [id];
  261. loadMonitoringData(id);
  262. }
  263. };
  264. const handleExpand = (expanded, record) => {
  265. // 确保展开状态与 expandedRowKeys 同步
  266. if (expanded) {
  267. expandedRowKeys.value = [record.id];
  268. } else {
  269. expandedRowKeys.value = [];
  270. }
  271. };
  272. // 加载监测数据
  273. let timer: null | NodeJS.Timeout = null;
  274. async function loadMonitoringData(deviceId: string) {
  275. // 先清除之前的定时器
  276. if (timer) {
  277. clearInterval(timer);
  278. timer = null;
  279. }
  280. // 立即请求一次数据
  281. // 这里可以加上初始加载,避免等待1秒的间隔
  282. // 定时器会在1秒后开始,所以先手动加载一次
  283. refreshData(deviceId);
  284. // 设置新的定时器
  285. timer = setInterval(() => {
  286. refreshData(deviceId);
  287. }, 1000);
  288. }
  289. async function refreshData(deviceId: string) {
  290. // 这里实现具体的请求逻辑
  291. const device = deviceList.value.find((d) => d.id === deviceId);
  292. const result = await getDevMonitorListById({ devId: deviceId.toString() });
  293. monitorList.value = Object.values(result.readData);
  294. }
  295. onMounted(async () => {
  296. await getSubStationList();
  297. await getDeviceList();
  298. });
  299. onUnmounted(() => {
  300. if (timer) {
  301. clearInterval(timer);
  302. timer = undefined;
  303. }
  304. });
  305. </script>
  306. <style lang="less" scoped>
  307. .safetyList {
  308. width: calc(100% - 20px);
  309. height: calc(100% - 80px);
  310. position: relative;
  311. margin: 70px 10px 10px 10px;
  312. .content {
  313. position: relative;
  314. width: 100%;
  315. height: 100%;
  316. .tab-box {
  317. display: flex;
  318. color: #fff;
  319. position: relative;
  320. background: linear-gradient(#001325, #012e4f);
  321. :deep(.zxm-tabs-nav) {
  322. margin: 0 !important;
  323. .zxm-tabs-tab {
  324. width: 180px;
  325. height: 45px;
  326. background: url('/@/assets/images/top-btn.png') center no-repeat;
  327. background-size: cover;
  328. display: flex;
  329. justify-content: center;
  330. font-size: 16px;
  331. }
  332. .zxm-tabs-tab-active {
  333. width: 180px;
  334. position: relative;
  335. background: url('/@/assets/images/top-btn-select.png') center no-repeat;
  336. background-size: cover;
  337. .zxm-tabs-tab-btn {
  338. color: #fff !important;
  339. }
  340. }
  341. .zxm-tabs-ink-bar {
  342. width: 0 !important;
  343. }
  344. .zxm-tabs-tab + .zxm-tabs-tab {
  345. margin: 0 !important;
  346. }
  347. }
  348. }
  349. .box-content {
  350. height: calc(100% - 50px);
  351. padding-top: 10px;
  352. box-sizing: border-box;
  353. .now-content {
  354. position: relative;
  355. width: 100%;
  356. height: 100%;
  357. display: flex;
  358. justify-content: space-between;
  359. align-items: center;
  360. .left-box {
  361. width: 30%;
  362. height: 100%;
  363. margin-right: 15px;
  364. padding: 10px;
  365. box-sizing: border-box;
  366. background: url('/@/assets/images/fire/bj1.png') no-repeat center;
  367. background-size: 100% 100%;
  368. border: 3px, solid, #0b69b6;
  369. border-radius: 5px;
  370. .left-content {
  371. display: flex;
  372. justify-content: flex-start;
  373. align-items: flex-start;
  374. flex-wrap: wrap;
  375. overflow-y: auto;
  376. .card-box {
  377. position: relative;
  378. width: 182px;
  379. height: 120px;
  380. margin-bottom: 30px;
  381. display: flex;
  382. justify-content: center;
  383. .card-itemN {
  384. position: relative;
  385. width: 85px;
  386. height: 120px;
  387. background: url('/@/assets/images/zd-2.png') no-repeat center;
  388. background-size: 100% 100%;
  389. cursor: pointer;
  390. .card-item-label {
  391. width: 100%;
  392. position: absolute;
  393. bottom: 5px;
  394. font-size: 12px;
  395. color: #fff;
  396. text-align: center;
  397. }
  398. }
  399. .card-itemL {
  400. position: relative;
  401. width: 85px;
  402. height: 120px;
  403. background: url('/@/assets/images/zd-3.png') no-repeat center;
  404. background-size: 100% 100%;
  405. cursor: pointer;
  406. .card-item-label {
  407. width: 100%;
  408. position: absolute;
  409. bottom: 5px;
  410. font-size: 12px;
  411. color: #fff;
  412. text-align: center;
  413. }
  414. }
  415. .card-itemD {
  416. position: relative;
  417. width: 85px;
  418. height: 120px;
  419. background: url('/@/assets/images/zd-1.png') no-repeat center;
  420. background-size: 100% 100%;
  421. cursor: pointer;
  422. .card-item-label {
  423. width: 100%;
  424. position: absolute;
  425. bottom: 5px;
  426. font-size: 12px;
  427. color: #fff;
  428. text-align: center;
  429. }
  430. }
  431. .card-modal {
  432. width: 86px;
  433. position: absolute;
  434. left: 140px;
  435. color: #fff;
  436. top: 50%;
  437. transform: translate(0, -50%);
  438. font-size: 12px;
  439. }
  440. .card-modal1 {
  441. width: 86px;
  442. position: absolute;
  443. left: -42px;
  444. color: #fff;
  445. top: 50%;
  446. transform: translate(0, -50%);
  447. font-size: 12px;
  448. }
  449. }
  450. }
  451. }
  452. .right-box {
  453. width: calc(70% - 15px);
  454. height: 100%;
  455. padding: 10px;
  456. box-sizing: border-box;
  457. background: url('/@/assets/images/fire/bj1.png') no-repeat center;
  458. background-size: 100% 100%;
  459. border: 3px, solid, #0b69b6;
  460. border-radius: 5px;
  461. .right-title {
  462. display: flex;
  463. height: 30px;
  464. align-items: center;
  465. font-size: 14px;
  466. color: #fff;
  467. margin-bottom: 10px;
  468. }
  469. }
  470. }
  471. }
  472. }
  473. }
  474. .down-btn {
  475. line-height: 15px;
  476. height: 20px;
  477. padding: 0px 17px;
  478. font-size: 12px;
  479. }
  480. .zxm-form {
  481. width: 50%;
  482. height: 100%;
  483. padding-top: 20px !important;
  484. box-sizing: border-box;
  485. }
  486. .zxm-picker,
  487. .zxm-input {
  488. border: 1px solid #3ad8ff77 !important;
  489. background-color: #ffffff !important;
  490. color: #fff !important;
  491. }
  492. .card-item.selected {
  493. border: 2px solid #3ad8ff77;
  494. /* 选中时的边框颜色 */
  495. }
  496. ::v-deep(.zxm-radio-wrapper) {
  497. font-size: 12px;
  498. }
  499. ::v-deep(.zxm-input) {
  500. font-size: 12px;
  501. }
  502. ::v-deep(.zxm-select:not(.zxm-select-customize-input) .zxm-select-selector) {
  503. border: 1px solid #3ad8ff77 !important;
  504. }
  505. // ::v-deep(.zxm-select-selection-item) {
  506. // color: #fff ;
  507. // }
  508. // ::v-deep(.zxm-form-item-label > label) {
  509. // color: #fff !important;
  510. // }
  511. /* 值样式 */
  512. .high-value {
  513. color: #f5222d;
  514. font-weight: bold;
  515. }
  516. .low-value {
  517. color: #1890ff;
  518. font-weight: bold;
  519. }
  520. .normal-value {
  521. color: #52c41a;
  522. }
  523. /* 嵌套表格样式 */
  524. :deep(.ant-table-expanded-row) > td {
  525. background-color: #f9f9f9 !important;
  526. padding: 0 !important;
  527. }
  528. :deep(.ant-table-expanded-row .ant-table) {
  529. margin: -10px -8px;
  530. background: #f9f9f9;
  531. }
  532. /* 自定义展开按钮 */
  533. :deep(.ant-table-row-expand-icon) {
  534. margin-right: 8px;
  535. }
  536. .device-select-box {
  537. margin-top: 60px;
  538. width: 208px;
  539. height: calc(100% - 70px);
  540. overflow-y: auto;
  541. color: #fff;
  542. :deep(.zxm-tree) {
  543. background: transparent !important;
  544. color: #fff !important;
  545. .zxm-tree-switcher {
  546. background: transparent !important;
  547. }
  548. .zxm-tree-node-content-wrapper.zxm-tree-node-selected {
  549. background-color: var(--tree-node-select);
  550. }
  551. .zxm-tree-node-content-wrapper:hover {
  552. background-color: var(--tree-node-hover);
  553. }
  554. input {
  555. height: 0px !important;
  556. }
  557. }
  558. &::-webkit-scrollbar-track {
  559. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  560. border-radius: 10px;
  561. background: #ededed22;
  562. height: 100px;
  563. }
  564. &::-webkit-scrollbar-thumb {
  565. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  566. background: #4288a444;
  567. }
  568. }
  569. .device-select {
  570. width: 250px;
  571. height: calc(100% - 70px);
  572. background: var(--image-tree-bg) no-repeat;
  573. position: fixed;
  574. top: 100px;
  575. left: 55px;
  576. background-size: contain;
  577. pointer-events: auto;
  578. padding: 20px 10px 30px 10px;
  579. }
  580. </style>
  581. <style>
  582. div[aria-hidden='true'] {
  583. display: none !important;
  584. }
  585. </style>