index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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-tab-pane tab="历史数据" key="history" />
  8. </a-tabs>
  9. <div class="box-content">
  10. <!-- 分站监测 -->
  11. <div class="now-content">
  12. <div class="left-box">
  13. <div class="device-select">
  14. <div class="device-select-box">
  15. <a-tree
  16. v-if="treeData && treeData.length > 0"
  17. :show-line="true"
  18. :tree-data="treeData"
  19. v-model:selectedKeys="selectedKeys"
  20. v-model:expandedKeys="expandedKeys"
  21. :defaultExpandAll="true"
  22. @select="onSelect"
  23. >
  24. </a-tree>
  25. </div>
  26. </div>
  27. </div>
  28. <div class="right-box" v-if="activeKey == 'device'">
  29. <div class="right-title">实时监测:</div>
  30. <a-table
  31. size="small"
  32. :scroll="{ y: 650 }"
  33. :columns="outerColumns"
  34. :data-source="deviceList"
  35. :pagination="false"
  36. :row-key="(record) => record.id"
  37. :expand-row-by-click="true"
  38. :expanded-row-keys="expandedRowKeys"
  39. @expand="onExpand"
  40. :loading="loading"
  41. >
  42. <!-- 自定义展开图标 -->
  43. <template #expandIcon="{ expanded, onExpand, record }">
  44. <a-button
  45. type="text"
  46. size="small"
  47. @click="
  48. (e) => {
  49. e.stopPropagation();
  50. toggleExpand(record.id);
  51. }
  52. "
  53. >
  54. <DownCircleTwoTone v-if="expandedRowKeys.includes(record.id)" />
  55. <RightCircleTwoTone v-else />
  56. </a-button>
  57. </template>
  58. <!-- 嵌套表格 -->
  59. <template #expandedRowRender="{ record }">
  60. <a-table
  61. size="small"
  62. :columns="innerColumns"
  63. :data-source="monitorList[record.id] || []"
  64. :pagination="false"
  65. :loading="loadingMap[record.id]"
  66. bordered
  67. :scroll="{ y: 410 }"
  68. >
  69. <template #bodyCell="{ column, record: innerRecord }">
  70. <template v-if="column.dataIndex === 'value'">
  71. <span>
  72. {{ innerRecord.value }}
  73. </span>
  74. </template>
  75. </template>
  76. </a-table>
  77. </template>
  78. <template #bodyCell="{ column, record }">
  79. <template v-if="column.key === 'netStatus'">
  80. <span
  81. :style="{
  82. color: record.netStatus ? '#52c41a' : '#ddd',
  83. fontWeight: '500',
  84. }"
  85. >
  86. {{ record.netStatus ? '在线' : '断开' }}
  87. </span>
  88. </template>
  89. <template v-else>
  90. {{ record[column.dataIndex] }}
  91. </template>
  92. </template>
  93. </a-table>
  94. </div>
  95. <div class="right-box" v-else-if="activeKey == 'history'">
  96. <template v-if="deviceType.startsWith('fanmain')">
  97. <HistoryTableFan class="w-100% h-100%" :device-code="`${deviceType}`" :scroll="scroll" dict-code="fan_dict" />
  98. </template>
  99. <template v-else-if="deviceType.startsWith('fanlocal')">
  100. <HistoryTableFan class="w-100% h-100%" :device-code="`${deviceType}`" :scroll="scroll" dict-code="fanlocal_dict" />s
  101. </template>
  102. <template v-else>
  103. <HistoryTable
  104. ref="historyTable"
  105. :sysId="systemID"
  106. :columns-type="`${deviceType}`"
  107. :device-type="deviceType"
  108. designScope="device-history"
  109. :scroll="scroll"
  110. />
  111. </template>
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. </template>
  118. <script setup lang="ts">
  119. import { ref, nextTick, reactive, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue';
  120. import { usePermission } from '/@/hooks/web/usePermission';
  121. import customHeader from '/@/components/vent/customHeader.vue';
  122. import { message, TreeProps } from 'ant-design-vue';
  123. import { getDeviceTypeList, getDeviceListByType, getDevMonitorListById } from './device.api';
  124. import HistoryTableFan from './history/HistoryTableFan.vue';
  125. import HistoryTable from './history/HistoryTable.vue';
  126. import { RightCircleTwoTone, DownCircleTwoTone } from '@ant-design/icons-vue';
  127. import { useRoute } from 'vue-router';
  128. import { useListPage } from '/@/hooks/system/useListPage';
  129. import { BasicTable } from '/@/components/Table';
  130. import { FormSchema } from '/@/components/Form/index';
  131. let route = useRoute();
  132. const { hasPermission } = usePermission();
  133. let activeKey = ref('device');
  134. const treeData = ref<TreeProps['treeData']>([]);
  135. const selectedKeys = ref<string[]>([]);
  136. const expandedKeys = ref<string[]>([]);
  137. const AllNodeKeys = ref<string[]>([]);
  138. const deviceType = ref(''); // 监测设备类型
  139. const systemType = ref('');
  140. const systemID = ref(''); // 系统监测时,系统id
  141. const dataSource = shallowRef([]); // 实时监测数据
  142. let startMonitorTimer = 0;
  143. const monitorTable = ref();
  144. const isRefresh = ref(true);
  145. const treeNodeTitle = ref(''); // 选中的树形标题
  146. const deviceList = ref<any[]>([]); // 设备列表
  147. const monitorList = ref<Record<string, any[]>>({}); // 监测数据列表
  148. const loading = ref(false);
  149. // 当前展开的行key数组
  150. const expandedRowKeys = ref([]);
  151. const scroll = reactive({
  152. y: 680,
  153. });
  154. // 加载状态映射
  155. const loadingMap = reactive({});
  156. // 切换tab页面
  157. async function onChangeTab(tab) {
  158. activeKey.value = tab;
  159. if (activeKey.value == 'device') {
  160. // 获取监测接口
  161. } else if (activeKey.value == 'history') {
  162. // 获取历史数据
  163. }
  164. }
  165. //树形菜单选择事件
  166. const onSelect: TreeProps['onSelect'] = (keys, e) => {
  167. deviceType.value = '';
  168. systemID.value = '';
  169. deviceList.value = [];
  170. const currentNode = e.node;
  171. const parentNode = currentNode.parent?.node;
  172. let newDeviceType = '';
  173. if (parentNode && parentNode.type.toString().startsWith('sys')) {
  174. systemType.value = parentNode.type;
  175. newDeviceType = parentNode.type;
  176. systemID.value = currentNode.type;
  177. } else {
  178. systemType.value = currentNode.type;
  179. newDeviceType = currentNode.type;
  180. }
  181. deviceType.value = newDeviceType;
  182. getDeviceList(newDeviceType);
  183. };
  184. // 获取所有节点key的函数
  185. const getAllNodeKeys = (nodes, type) => {
  186. const keys = [];
  187. const traverse = (nodeList) => {
  188. nodeList.forEach((node) => {
  189. if (node.type === type) {
  190. selectedKeys.value.push(node.key);
  191. }
  192. keys.push(node.key);
  193. if (node.children && node.children.length > 0) {
  194. traverse(node.children);
  195. }
  196. });
  197. };
  198. traverse(nodes);
  199. return keys;
  200. };
  201. // 获取树形菜单数据
  202. async function getDeviceType(type?) {
  203. const result = await getDeviceTypeList({});
  204. if (result.length > 0) {
  205. const dataSource = [];
  206. let key = '0';
  207. const getData = (resultList, dataSourceList, keyVal) => {
  208. resultList.forEach((item, index) => {
  209. const children = item.children ? getData(item.children, [], `${keyVal}-${index}`) : [];
  210. dataSourceList.push({
  211. children: children,
  212. title: item.itemText,
  213. key: `${keyVal}-${index}`,
  214. type: item.itemValue,
  215. parentKey: `${keyVal}`,
  216. });
  217. });
  218. return dataSourceList;
  219. };
  220. treeData.value = getData(result, dataSource, key);
  221. // 数据就绪后设置展开key数组
  222. expandedKeys.value = getAllNodeKeys(treeData.value, type);
  223. }
  224. }
  225. // 获取当前选择节点
  226. // 根据选择设备获取设备列表
  227. async function getDeviceList(deviceTypeVal?: any) {
  228. // 1. 如果没有设备类型值,停止定时器并返回(不再重复请求)
  229. if (!deviceTypeVal) {
  230. if (timer) {
  231. clearInterval(timer);
  232. timer = undefined;
  233. }
  234. return;
  235. }
  236. if (timer) {
  237. clearInterval(timer);
  238. timer = undefined;
  239. }
  240. loading.value = true;
  241. const fetchDeviceData = async () => {
  242. const params: any = {
  243. devKind: deviceTypeVal,
  244. pageNo: 1,
  245. pageSize: 2000,
  246. };
  247. try {
  248. const result = await getDeviceListByType(params);
  249. loading.value = false;
  250. deviceList.value = result.records; // 更新设备列表
  251. } catch (error) {
  252. console.error('定时请求设备列表失败:', error);
  253. }
  254. };
  255. await fetchDeviceData();
  256. timer = setInterval(fetchDeviceData, 3000);
  257. }
  258. // 外层表格列配置
  259. const outerColumns = [
  260. {
  261. title: '设备ID',
  262. dataIndex: 'id',
  263. key: 'id',
  264. align: 'center',
  265. },
  266. {
  267. title: '安装位置',
  268. dataIndex: 'strinstallpos',
  269. key: 'strinstallpos',
  270. align: 'center',
  271. },
  272. {
  273. title: '设备类型',
  274. dataIndex: 'devicekind_dictText',
  275. key: 'devicekind_dictText',
  276. align: 'center',
  277. },
  278. {
  279. title: '状态',
  280. dataIndex: 'netStatus',
  281. key: 'netStatus',
  282. align: 'center',
  283. },
  284. ];
  285. // 内层表格列配置
  286. const innerColumns = [
  287. {
  288. title: '地址',
  289. dataIndex: 'plcAddr',
  290. key: 'plcAddr',
  291. align: 'center',
  292. },
  293. {
  294. title: '数据code',
  295. dataIndex: 'valueCode',
  296. align: 'center',
  297. key: 'valueCode',
  298. },
  299. {
  300. title: '数据名称',
  301. dataIndex: 'valueName',
  302. align: 'center',
  303. key: 'valueName',
  304. },
  305. {
  306. title: '数据值',
  307. dataIndex: 'value',
  308. align: 'center',
  309. key: 'value',
  310. },
  311. {
  312. title: '时间',
  313. dataIndex: 'time',
  314. align: 'center',
  315. key: 'time',
  316. },
  317. ];
  318. // 切换展开状态
  319. const toggleExpand = (deviceId) => {
  320. const index = expandedRowKeys.value.indexOf(deviceId);
  321. if (index > -1) {
  322. // 如果已经展开,则关闭
  323. expandedRowKeys.value.splice(index, 1);
  324. if (timer) {
  325. clearInterval(timer);
  326. timer = undefined;
  327. }
  328. } else {
  329. // 如果未展开,则打开
  330. expandedRowKeys.value.push(deviceId);
  331. loadMonitoringData(deviceId);
  332. }
  333. };
  334. // 加载监测数据
  335. let timer: null | NodeJS.Timeout = null;
  336. async function loadMonitoringData(deviceId: string) {
  337. // 先清除之前的定时器
  338. if (timer) {
  339. clearInterval(timer);
  340. timer = null;
  341. }
  342. // 定时器会在1秒后开始,所以先手动加载一次
  343. refreshData(deviceId);
  344. // 设置新的定时器
  345. timer = setInterval(() => {
  346. refreshData(deviceId);
  347. }, 1000);
  348. }
  349. async function refreshData(deviceId: string) {
  350. // 这里实现具体的请求逻辑
  351. const device = deviceList.value.find((d) => d.id === deviceId);
  352. const result = await getDevMonitorListById({ devId: deviceId.toString() });
  353. monitorList.value[deviceId] = Object.values(result.readData);
  354. console.log(monitorList.value[deviceId], '123123');
  355. }
  356. onMounted(() => {
  357. const path = route.query.deviceType;
  358. if (path) {
  359. getDeviceType(path);
  360. getDeviceList(path);
  361. } else {
  362. getDeviceType();
  363. }
  364. });
  365. onUnmounted(() => {
  366. if (timer) {
  367. clearInterval(timer);
  368. timer = undefined;
  369. }
  370. });
  371. // 监听分页变化
  372. </script>
  373. <style lang="less" scoped>
  374. .safetyList {
  375. width: calc(100% - 20px);
  376. height: calc(100% - 80px);
  377. position: relative;
  378. margin: 70px 10px 10px 10px;
  379. .content {
  380. position: relative;
  381. width: 100%;
  382. height: 100%;
  383. .tab-box {
  384. display: flex;
  385. color: #fff;
  386. position: relative;
  387. background: linear-gradient(#001325, #012e4f);
  388. :deep(.zxm-tabs-nav) {
  389. margin: 0 !important;
  390. .zxm-tabs-tab {
  391. width: 180px;
  392. height: 45px;
  393. background: url('/@/assets/images/top-btn.png') center no-repeat;
  394. background-size: cover;
  395. display: flex;
  396. justify-content: center;
  397. font-size: 16px;
  398. }
  399. .zxm-tabs-tab-active {
  400. width: 180px;
  401. position: relative;
  402. background: url('/@/assets/images/top-btn-select.png') center no-repeat;
  403. background-size: cover;
  404. .zxm-tabs-tab-btn {
  405. color: #fff !important;
  406. }
  407. }
  408. .zxm-tabs-ink-bar {
  409. width: 0 !important;
  410. }
  411. .zxm-tabs-tab + .zxm-tabs-tab {
  412. margin: 0 !important;
  413. }
  414. }
  415. }
  416. .box-content {
  417. height: calc(100% - 50px);
  418. padding-top: 10px;
  419. box-sizing: border-box;
  420. .now-content {
  421. position: relative;
  422. width: 100%;
  423. height: 100%;
  424. display: flex;
  425. justify-content: space-between;
  426. align-items: center;
  427. .left-box {
  428. width: 20%;
  429. height: 100%;
  430. margin-right: 15px;
  431. padding: 10px;
  432. box-sizing: border-box;
  433. background: url('/@/assets/images/fire/bj1.png') no-repeat center;
  434. background-size: 100% 100%;
  435. border: 3px, solid, #0b69b6;
  436. border-radius: 5px;
  437. }
  438. .right-box {
  439. width: calc(80% - 15px);
  440. height: 100%;
  441. padding: 10px;
  442. box-sizing: border-box;
  443. background: url('/@/assets/images/fire/bj1.png') no-repeat center;
  444. background-size: 100% 100%;
  445. border: 3px, solid, #0b69b6;
  446. border-radius: 5px;
  447. .right-title {
  448. display: flex;
  449. height: 30px;
  450. align-items: center;
  451. font-size: 14px;
  452. color: #fff;
  453. margin-bottom: 10px;
  454. }
  455. }
  456. }
  457. }
  458. }
  459. }
  460. .down-btn {
  461. line-height: 15px;
  462. height: 20px;
  463. padding: 0px 17px;
  464. font-size: 12px;
  465. }
  466. .zxm-form {
  467. width: 50%;
  468. height: 100%;
  469. padding-top: 20px !important;
  470. box-sizing: border-box;
  471. }
  472. .zxm-picker,
  473. .zxm-input {
  474. border: 1px solid #3ad8ff77 !important;
  475. background-color: #ffffff !important;
  476. color: #fff !important;
  477. }
  478. .card-item.selected {
  479. border: 2px solid #3ad8ff77;
  480. /* 选中时的边框颜色 */
  481. }
  482. ::v-deep(.zxm-radio-wrapper) {
  483. font-size: 12px;
  484. }
  485. ::v-deep(.zxm-input) {
  486. font-size: 12px;
  487. }
  488. ::v-deep(.zxm-select:not(.zxm-select-customize-input) .zxm-select-selector) {
  489. border: 1px solid #3ad8ff77 !important;
  490. }
  491. // ::v-deep(.zxm-select-selection-item) {
  492. // color: #fff ;
  493. // }
  494. // ::v-deep(.zxm-form-item-label > label) {
  495. // color: #fff !important;
  496. // }
  497. /* 值样式 */
  498. .high-value {
  499. color: #f5222d;
  500. font-weight: bold;
  501. }
  502. .low-value {
  503. color: #1890ff;
  504. font-weight: bold;
  505. }
  506. .normal-value {
  507. color: #52c41a;
  508. }
  509. /* 嵌套表格样式 */
  510. :deep(.ant-table-expanded-row) > td {
  511. background-color: #f9f9f9 !important;
  512. padding: 0 !important;
  513. }
  514. :deep(.ant-table-expanded-row .ant-table) {
  515. margin: -10px -8px;
  516. background: #f9f9f9;
  517. }
  518. /* 自定义展开按钮 */
  519. :deep(.ant-table-row-expand-icon) {
  520. margin-right: 8px;
  521. }
  522. .device-select-box {
  523. margin-top: 30px;
  524. width: 300px;
  525. height: calc(100% - 70px);
  526. overflow-y: auto;
  527. color: #fff;
  528. :deep(.zxm-tree) {
  529. background: transparent !important;
  530. color: #fff !important;
  531. .zxm-tree-switcher {
  532. background: transparent !important;
  533. }
  534. .zxm-tree-node-content-wrapper.zxm-tree-node-selected {
  535. background-color: #00b1c8;
  536. }
  537. .zxm-tree-node-content-wrapper:hover {
  538. background-color: #00b1c8;
  539. }
  540. input {
  541. height: 0px !important;
  542. }
  543. }
  544. &::-webkit-scrollbar-track {
  545. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  546. border-radius: 10px;
  547. background: #ededed22;
  548. height: 100px;
  549. }
  550. &::-webkit-scrollbar-thumb {
  551. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  552. background: #4288a444;
  553. }
  554. }
  555. .device-select {
  556. width: 280px !important;
  557. height: calc(100% - 70px);
  558. background: var(--image-tree-bg) no-repeat;
  559. position: fixed;
  560. top: 100px;
  561. left: 55px;
  562. background-size: contain;
  563. pointer-events: auto;
  564. padding: 20px 10px 30px 10px;
  565. }
  566. /* 在线状态 - 绿色 */
  567. .status-online {
  568. color: #36d399; /* 可替换为其他绿色,如 #00C48C */
  569. font-weight: 500;
  570. }
  571. /* 断开状态 - 灰色(可选调整为红色) */
  572. .status-offline {
  573. color: #999;
  574. /* 若想断开显示红色:color: #F5222D; */
  575. }
  576. </style>
  577. <style>
  578. div[aria-hidden='true'] {
  579. display: none !important;
  580. }
  581. </style>