PointMonitor.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <template>
  2. <div class="point-monitor-page">
  3. <div class="monitor-layout">
  4. <a-card :bordered="false" class="menu-panel" :body-style="{ padding: '8px 0' }">
  5. <a-menu :open-keys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline" @title-click="handleChannelClick" @click="handleDeviceClick">
  6. <a-sub-menu v-for="channel in channels" :key="'c-' + channel.id">
  7. <template #title>{{ channel.name }}</template>
  8. <a-menu-item v-for="device in channel.deviceList || []" :key="'d-' + device.id">{{ device.name }}</a-menu-item>
  9. </a-sub-menu>
  10. </a-menu>
  11. </a-card>
  12. <div class="content-panel">
  13. <a-card :bordered="false">
  14. <a-form :model="queryParams" layout="inline">
  15. <a-form-item label="名称">
  16. <a-input v-model:value="queryParams.name" allow-clear placeholder="请输入名称" />
  17. </a-form-item>
  18. <a-form-item label="编码">
  19. <a-input v-model:value="queryParams.code" allow-clear placeholder="请输入编码" />
  20. </a-form-item>
  21. <a-form-item label="地址">
  22. <a-input-number v-model:value="queryParams.addr" :precision="0" :controls="false" placeholder="请输入地址" class="query-number" />
  23. </a-form-item>
  24. <a-form-item label="分类">
  25. <a-select v-model:value="queryParams.category" class="query-select" :options="categoryOptions" @change="handleCategoryChange" />
  26. </a-form-item>
  27. <a-form-item>
  28. <a-space>
  29. <a-button type="primary" :loading="loading" @click="handleQuery"><BearJiaIcon icon="SearchOutlined"/>查询</a-button>
  30. <a-button @click="resetQuery"><BearJiaIcon icon="ReloadOutlined"/>重置</a-button>
  31. <a-button v-if="countdown > 0" disabled>{{ countdown }}s后刷新数据</a-button>
  32. </a-space>
  33. </a-form-item>
  34. </a-form>
  35. </a-card>
  36. <a-card :bordered="false" class="section">
  37. <a-table
  38. :columns="columns"
  39. :data-source="rows"
  40. :loading="loading"
  41. :pagination="pagination"
  42. row-key="code"
  43. size="middle"
  44. :scroll="tableScroll"
  45. @change="handleTableChange"
  46. >
  47. <template #bodyCell="{ column, record }">
  48. <template v-if="column.key === 'tmReport'">{{ formatTime(record.tmReport) }}</template>
  49. <template v-else-if="column.key === 'tmChange'">{{ formatTime(record.tmChange) }}</template>
  50. </template>
  51. </a-table>
  52. </a-card>
  53. </div>
  54. </div>
  55. </div>
  56. </template>
  57. <script setup name="PointMonitor">
  58. import { computed, onBeforeUnmount, reactive, ref } from 'vue';
  59. import dayjs from 'dayjs';
  60. import { cascadeMonitorChannel } from '@/api/config/channel';
  61. import { pointMonitor } from '@/api/config/point';
  62. import { BearJiaIcon } from '@/utils/BearJiaIcon.js';
  63. const CATEGORY_FIRST = 'YX';
  64. const categoryOptions = [
  65. { label: '遥信', value: 'YX', interval: 5 },
  66. { label: '遥测', value: 'YC', interval: 15 }
  67. // { label: '遥脉', value: 'YM' },
  68. // { label: '遥控', value: 'YK' },
  69. // { label: '遥调', value: 'YT' }
  70. ];
  71. const loading = ref(false);
  72. const rows = ref([]);
  73. const channels = ref([]);
  74. const openKeys = ref([]);
  75. const selectedKeys = ref([]);
  76. const currentDeviceId = ref(undefined);
  77. const queryParams = reactive({
  78. pageNum: 1,
  79. pageSize: 10,
  80. category: CATEGORY_FIRST,
  81. name: undefined,
  82. code: undefined,
  83. addr: undefined
  84. });
  85. const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true });
  86. // 自动刷新定时器: 按`分类`选中option的interval定时调用getList
  87. let dataRefreshInterval = null;
  88. // 倒计时秒表, 每1s递减
  89. let countdownInterval = null;
  90. // 距下次刷新的秒数, >0时显示倒计时按钮
  91. const countdown = ref(0);
  92. const currentCategoryOption = computed(() => categoryOptions.find((item) => item.value === queryParams.category));
  93. // 清空定时器和倒计时(x<=0时按钮隐藏)
  94. const clearDataRefresh = () => {
  95. if (dataRefreshInterval) {
  96. clearInterval(dataRefreshInterval);
  97. dataRefreshInterval = null;
  98. }
  99. if (countdownInterval) {
  100. clearInterval(countdownInterval);
  101. countdownInterval = null;
  102. }
  103. countdown.value = 0;
  104. };
  105. // 值列: 遥信(YX)取iValue, 遥测(YC)取dValue
  106. const columns = computed(() => [
  107. { title: '名称', dataIndex: 'name', width: 180, ellipsis: true },
  108. { title: '编码', dataIndex: 'code', width: 140, ellipsis: true },
  109. { title: '地址', dataIndex: 'addr', width: 100 },
  110. { title: '测点属性', dataIndex: 'pointAttr', width: 110 },
  111. { title: '值', dataIndex: queryParams.category === 'YX' ? 'ivalue' : 'dvalue', width: 120 },
  112. { title: '值属性', dataIndex: 'valueAttr', width: 110 },
  113. { title: '上报时间', dataIndex: 'tmReport', key: 'tmReport', width: 170 },
  114. { title: '变化时间', dataIndex: 'tmChange', key: 'tmChange', width: 170 }
  115. ]);
  116. const tableScroll = computed(() => ({ x: columns.value.reduce((sum, col) => sum + (col.width || 140), 0) + 40 }));
  117. // 计算2: 时间计算, timestamp 转 date(yyyy-MM-dd HH:mm:ss)
  118. const formatTime = (timestamp) => (timestamp && Number(timestamp) > 0 ? dayjs(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss') : '');
  119. const getList = async () => {
  120. // pointMonitor执行前, 若定时器已存在先清空, 避免重复叠加
  121. clearDataRefresh();
  122. if (currentDeviceId.value === undefined) {
  123. rows.value = [];
  124. pagination.total = 0;
  125. return;
  126. }
  127. loading.value = true;
  128. try {
  129. const res = await pointMonitor({ ...queryParams, configDeviceId: currentDeviceId.value });
  130. rows.value = res?.rows || [];
  131. pagination.total = res?.total || 0;
  132. pagination.current = queryParams.pageNum;
  133. pagination.pageSize = queryParams.pageSize;
  134. // 加载成功后, 按`分类`选中option的interval创建定时器并初始化倒计时; 失败即停不重建
  135. const interval = currentCategoryOption.value?.interval;
  136. if (interval) {
  137. countdown.value = interval;
  138. countdownInterval = setInterval(() => {
  139. if (countdown.value > 0) countdown.value -= 1;
  140. }, 1000);
  141. dataRefreshInterval = setInterval(getList, interval * 1000);
  142. }
  143. } catch {
  144. // 请求异常由axios拦截器统一提示, 此处失败即停
  145. } finally {
  146. loading.value = false;
  147. }
  148. };
  149. // 重置右侧筛选条件: 分类回到第一个option, 清空名称/编码/地址
  150. const resetFilter = () => {
  151. queryParams.category = CATEGORY_FIRST;
  152. queryParams.name = undefined;
  153. queryParams.code = undefined;
  154. queryParams.addr = undefined;
  155. queryParams.pageNum = 1;
  156. };
  157. // 选中设备并刷新数据
  158. const selectDevice = (device) => {
  159. currentDeviceId.value = device?.id;
  160. selectedKeys.value = device ? ['d-' + device.id] : [];
  161. resetFilter();
  162. getList();
  163. };
  164. // 事件1: 点击1级菜单 — 展开选中的通道、收起其他、选中第一个二级菜单、重置筛选并刷新
  165. const handleChannelClick = ({ key }) => {
  166. openKeys.value = [key];
  167. const channel = channels.value.find((item) => 'c-' + item.id === key);
  168. selectDevice(channel?.deviceList?.[0]);
  169. };
  170. // 事件2: 点击2级菜单 — 切换设备、分类回到第一个option、清空条件、重新加载
  171. const handleDeviceClick = ({ key }) => {
  172. if (!key.startsWith('d-')) return;
  173. const deviceId = Number(key.slice(2));
  174. const device = channels.value.flatMap((channel) => channel.deviceList || []).find((item) => item.id === deviceId);
  175. currentDeviceId.value = deviceId;
  176. resetFilter();
  177. getList();
  178. };
  179. // 事件4: 切换分类 — 条件分类置为当前选中值、清空名称/编码/地址、重新加载
  180. const handleCategoryChange = () => {
  181. queryParams.name = undefined;
  182. queryParams.code = undefined;
  183. queryParams.addr = undefined;
  184. queryParams.pageNum = 1;
  185. getList();
  186. };
  187. // 事件5: 查询数据
  188. const handleQuery = () => {
  189. queryParams.pageNum = 1;
  190. getList();
  191. };
  192. // 事件6: 重制搜索条件 — 清空名称/编码/地址
  193. const resetQuery = () => {
  194. queryParams.name = undefined;
  195. queryParams.code = undefined;
  196. queryParams.addr = undefined;
  197. };
  198. // 事件3: 分页查询
  199. const handleTableChange = (page) => {
  200. queryParams.pageNum = page.current;
  201. queryParams.pageSize = page.pageSize;
  202. getList();
  203. };
  204. const loadCascades = async () => {
  205. const res = await cascadeMonitorChannel();
  206. channels.value = res?.data || [];
  207. // 默认选中第一个一级菜单的第一个二级菜单
  208. const firstChannel = channels.value[0];
  209. if (firstChannel) {
  210. openKeys.value = ['c-' + firstChannel.id];
  211. selectDevice(firstChannel.deviceList?.[0]);
  212. } else {
  213. getList();
  214. }
  215. };
  216. // 浏览器tab隐藏时清空定时器, 恢复可见时立即刷新并重启倒计时
  217. const handleVisibilityChange = () => {
  218. if (document.hidden) {
  219. clearDataRefresh();
  220. } else {
  221. getList();
  222. }
  223. };
  224. document.addEventListener('visibilitychange', handleVisibilityChange);
  225. // 页面销毁时清空定时器
  226. onBeforeUnmount(() => {
  227. clearDataRefresh();
  228. document.removeEventListener('visibilitychange', handleVisibilityChange);
  229. });
  230. loadCascades();
  231. </script>
  232. <style lang="less" scoped>
  233. .point-monitor-page {
  234. .monitor-layout {
  235. display: flex;
  236. align-items: flex-start;
  237. }
  238. .menu-panel {
  239. flex: 0 0 200px;
  240. width: 200px;
  241. margin-right: 12px;
  242. :deep(.ant-menu) {
  243. border-inline-end: none;
  244. }
  245. }
  246. .content-panel {
  247. flex: 1;
  248. min-width: 0;
  249. }
  250. .section {
  251. margin-top: 12px;
  252. }
  253. .query-select {
  254. width: 140px;
  255. }
  256. .query-number {
  257. width: 120px;
  258. }
  259. }
  260. </style>