index.vue 16 KB

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