BasicMonitoring.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <template>
  2. <!-- 场景选择下拉框 -->
  3. <customHeader
  4. :fieldNames="{ label: 'systemname', value: 'id', options: 'children' }"
  5. :options="options"
  6. :optionValue="optionValue"
  7. @change="changeSelectRow"
  8. >
  9. {{ mainTitle }}
  10. </customHeader>
  11. <!-- 默认插槽,一般用来放模型 -->
  12. <slot :monitor-data="monitorData"></slot>
  13. <div class="scene-box">
  14. <div class="center-container">
  15. <!-- 监测模块 -->
  16. <template v-if="activeKey == 'monitor'">
  17. <slot name="opration">
  18. <div class="button-wrapper" v-if="monitorType !== 'nitrogenMonitor'">
  19. <template v-for="item in mainConfig.operations" :key="item.key">
  20. <template v-if="hasPermission(item.permission)">
  21. <div :class="{ 'button-box': true, 'button-box-disable': item.disabled }" @click="handleOperate(item)">{{ item.label }} </div>
  22. </template>
  23. </template>
  24. </div>
  25. </slot>
  26. <slot name="monitor">
  27. <ModuleCommon
  28. v-for="cfg in mainConfig.configs"
  29. :key="cfg.deviceType"
  30. :show-style="cfg.showStyle"
  31. :module-data="cfg.moduleData"
  32. :module-name="cfg.moduleName"
  33. :device-type="cfg.deviceType"
  34. :data="monitorData"
  35. :visible="true"
  36. />
  37. </slot>
  38. </template>
  39. <div v-else class="history-group">
  40. <!-- 场景下关联的设备有很多,在这里选择 -->
  41. <div v-if="showDeviceList && deviceList.length > 0" class="device-button-group">
  42. <div
  43. v-for="(device, index) in deviceList"
  44. :class="{ 'device-button': true, 'device-active': deviceActive == device.deviceType }"
  45. :key="index"
  46. @click="deviceChange(index)"
  47. >
  48. {{ device.deviceName }}
  49. </div>
  50. </div>
  51. <div class="history-container">
  52. <slot name="history" :device-type="deviceType" :device-id="optionValue">
  53. <HistoryTable
  54. v-if="activeKey == 'monitor_history'"
  55. class="vent-margin-t-20"
  56. :columns-type="deviceType"
  57. :device-type="deviceType"
  58. :sysId="optionValue"
  59. :scroll="{ y: 650 }"
  60. v-bind="monitorHistoryConfig"
  61. />
  62. </slot>
  63. <slot name="handler" :device-type="deviceType" :device-id="optionValue">
  64. <HandlerHistoryTable
  65. v-if="activeKey == 'handler_history'"
  66. class="vent-margin-t-20"
  67. columns-type="operator_history"
  68. :deviceType="deviceType"
  69. v-bind="handlerHistoryConfig"
  70. />
  71. </slot>
  72. <slot name="alarm" :device-type="deviceType" :device-id="optionValue">
  73. <AlarmHistoryTable
  74. v-if="activeKey == 'faultRecord'"
  75. columns-type="alarm"
  76. :device-type="deviceType"
  77. :sys-id="optionValue"
  78. v-bind="alarmHistoryConfig"
  79. />
  80. </slot>
  81. </div>
  82. </div>
  83. </div>
  84. <BottomMenu @change="changeActive" />
  85. </div>
  86. <PasswordModal
  87. :modal-is-show="passwordModalShown"
  88. :modal-title="operatingTarget?.label"
  89. @handle-ok="handlePasswordOK"
  90. @handle-cancel="passwordModalShown = false"
  91. />
  92. </template>
  93. <script setup lang="ts">
  94. import customHeader from '/@/components/vent/customHeader.vue';
  95. import { ref, onMounted, onUnmounted } from 'vue';
  96. import { getDevice, sysList } from '/@/views/vent/monitorManager/comment/comment.api';
  97. import BottomMenu from '/@/views/vent/comment/components/bottomMenu.vue';
  98. import ModuleCommon from '/@/views/vent/home/configurable/components/ModuleCommon.vue';
  99. import HistoryTable from '/@/views/vent/monitorManager/comment/HistoryTable.vue';
  100. import HandlerHistoryTable from '/@/views/vent/monitorManager/comment/HandlerHistoryTable.vue';
  101. import AlarmHistoryTable from '/@/views/vent/monitorManager/comment/AlarmHistoryTable.vue';
  102. import { useRouter } from 'vue-router';
  103. import { Config } from '/@/views/vent/deviceManager/configurationTable/types';
  104. import { message, Modal } from 'ant-design-vue';
  105. import _ from 'lodash';
  106. import PasswordModal from '/@/views/vent/monitorManager/comment/components/PasswordModal.vue';
  107. import { usePermission } from '/@/hooks/web/usePermission';
  108. import { deviceControlApi } from '/@/api/vent';
  109. type DeviceType = { deviceType: string; deviceName: string; datalist: any[] };
  110. type Operation = {
  111. label: string;
  112. key: string;
  113. value: string;
  114. disabled?: boolean;
  115. showPassword?: boolean;
  116. permission?: string;
  117. onOk?: (key: string, value: string, pwd?: string) => Promise<void>;
  118. };
  119. const props = withDefaults(
  120. defineProps<{
  121. mainTitle: string;
  122. /** 是否显示历史数据上方的设备类型选择器 */
  123. showDeviceList?: boolean;
  124. /** 请求场景数据传入的类型字符 */
  125. strtype: string;
  126. /** 请求场景数据传入的页面类型字符 */
  127. pagetype?: string;
  128. /** 获取各表格配置时依赖的设备类型 */
  129. monitorType?: string;
  130. /** 监测页面类型*/
  131. // deviceType: string;
  132. /** 主要模块配置 */
  133. mainConfig: {
  134. operations: Operation[];
  135. configs: Config[];
  136. /** 获取该场景所含设备及其监测信息的API */
  137. monitorApi?: (params: { deviceType: string; deviceId: number | string }) => Promise<any>;
  138. /** 定时获取监测信息的配置,单位为毫秒,不传入即默认,传0即停用 */
  139. timer?: number;
  140. };
  141. /** 历史数据配置 */
  142. monitorHistoryConfig: {
  143. /** 请求历史数据时传入的类型字符 */
  144. columnsType?: string;
  145. /** 如果默认的设备类型不适用,可以传递固定的类型 */
  146. deviceType?: string;
  147. /** 仅展示已绑定设备,选择是则从系统中获取sysId下已绑定设备。仅能查询到已绑定设备的历史数据 */
  148. onlyBounedDevices?: boolean;
  149. /** 显示历史数据曲线图 */
  150. showHistoryCurve?: boolean;
  151. };
  152. /** 操作历史配置 */
  153. handlerHistoryConfig: {
  154. /** 请求操作历史时传入的类型字符 */
  155. columnsType?: string;
  156. /** 如果默认的设备类型不适用,可以传递固定的类型 */
  157. deviceType?: string;
  158. /** 获取操作历史的API,可以不提供以使用默认的请求 */
  159. deviceListApi?: (params: any) => Promise<any[]>;
  160. };
  161. /** 报警历史配置 */
  162. alarmHistoryConfig: {
  163. /** 请求报警历史时传入的类型字符 */
  164. columnsType?: string;
  165. /** 如果默认的设备类型不适用,可以传递固定的类型 */
  166. deviceType?: string;
  167. /** 获取报警历史的API,可以不提供以使用默认的请求 */
  168. list?: (params: any) => Promise<any[]>;
  169. /** 获取设备以供报警历史过滤的API,可以不提供以使用默认的请求 */
  170. deviceListApi?: (params: any) => Promise<any[]>;
  171. };
  172. }>(),
  173. {
  174. mainConfig: () => ({
  175. operations: [],
  176. configs: [],
  177. }),
  178. monitorHistoryConfig: () => ({}),
  179. handlerHistoryConfig: () => ({}),
  180. alarmHistoryConfig: () => ({}),
  181. pagetype: 'normal',
  182. showDeviceList: true,
  183. }
  184. );
  185. const { currentRoute } = useRouter();
  186. const { hasPermission } = usePermission();
  187. const activeKey = ref('monitor');
  188. function changeActive(activeValue) {
  189. activeKey.value = activeValue;
  190. }
  191. /** 场景选项 */
  192. const options = ref([]);
  193. /** 已选择了的场景的id */
  194. const optionValue = ref('');
  195. /** 获取左上角场景选择框数据的方法,如果此时初始场景未赋值则选择首项做初始化 */
  196. async function getSysDataSource() {
  197. const res = await sysList({ strtype: props.strtype, pagetype: props.pagetype }).catch(() => {
  198. message.error('获取场景数据时发生了错误');
  199. });
  200. // 初始时选择第一条数据
  201. options.value = res.records || [];
  202. if (!optionValue.value) {
  203. changeSelectRow(options.value[0]['id']);
  204. }
  205. }
  206. // 切换检测数据
  207. function changeSelectRow(deviceID) {
  208. optionValue.value = deviceID;
  209. if (props.monitorType !== 'nitrogenMonitor') {
  210. getDeviceList();
  211. }
  212. }
  213. /** 当前场景所关联设备 */
  214. const deviceList = ref<DeviceType[]>([]);
  215. const deviceActive = ref('');
  216. const deviceType = ref('');
  217. /** 选择设备 */
  218. function deviceChange(index) {
  219. deviceType.value = deviceList.value[index]?.deviceType || '';
  220. deviceActive.value = deviceList.value[index]?.deviceType || '';
  221. }
  222. /** 查询当前场景所关联设备列表 */
  223. async function getDeviceList() {
  224. const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value }).catch(() => {
  225. message.error('获取已绑定设备时发生了错误');
  226. });
  227. deviceList.value = msgTxt.reduce((arr, item) => {
  228. const data = item.datalist.map((data: any) => {
  229. return Object.assign(data, data.readData);
  230. });
  231. // sys代表场景本身,应该过滤掉去处理该场景下的关联设备
  232. if (item.type != 'sys') {
  233. arr.unshift({
  234. deviceType: item.type,
  235. deviceName: item.typeName || item.datalist[0].typeName,
  236. datalist: data,
  237. });
  238. }
  239. return arr;
  240. }, []);
  241. if (!deviceActive.value) {
  242. deviceChange(0);
  243. }
  244. }
  245. let timer: NodeJS.Timeout;
  246. /** 场景的监测数据 */
  247. const monitorData = ref<any>({});
  248. /** 获取本场景下所绑定的设备,将监测数据赋值 */
  249. async function getMonitor() {
  250. if (props.mainConfig.monitorApi) {
  251. monitorData.value = await props.mainConfig
  252. .monitorApi({
  253. deviceType: deviceType.value,
  254. deviceId: optionValue.value,
  255. })
  256. .catch(() => {
  257. message.error('获取已绑定设备时发生了错误');
  258. });
  259. } else if (optionValue.value) {
  260. const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value }).catch(() => {
  261. message.error('获取已绑定设备时发生了错误');
  262. });
  263. const temp = {};
  264. msgTxt.forEach((item) => {
  265. _.set(temp, item.type, item.datalist);
  266. });
  267. monitorData.value = temp;
  268. }
  269. }
  270. /** 密码提示框是否显示 */
  271. const passwordModalShown = ref(false);
  272. /** 下发操作时的目标配置 */
  273. const operatingTarget = ref<Operation>();
  274. /** 操作按钮点击后根据配置弹出确认框,初始化数据 */
  275. function handleOperate(item: Operation) {
  276. if (item.disabled) return;
  277. operatingTarget.value = item;
  278. if (item.showPassword) {
  279. passwordModalShown.value = true;
  280. } else {
  281. Modal.confirm({
  282. title: '操作确认',
  283. content: `确定要进行${operatingTarget.value.label}操作吗?`,
  284. iconType: 'info',
  285. onOk: () => handlePasswordOK(),
  286. });
  287. }
  288. }
  289. /** 密码输入后确认的回调 */
  290. function handlePasswordOK(pwd?: string) {
  291. if (!operatingTarget.value) return message.error('操作目标不存在');
  292. const { onOk = deviceControl, key, value } = operatingTarget.value;
  293. return onOk(key, value, pwd)
  294. .then(() => {
  295. passwordModalShown.value = false;
  296. })
  297. .finally(() => {
  298. operatingTarget.value = undefined;
  299. });
  300. }
  301. function deviceControl(key: string, value: string, pwd?: string) {
  302. return deviceControlApi({
  303. deviceid: optionValue.value,
  304. devicetype: deviceType.value,
  305. password: pwd,
  306. paramcode: key,
  307. value: value,
  308. })
  309. .then((r) => {
  310. if (!r.success) throw r.message || '操作失败';
  311. message.success('指令下发成功');
  312. })
  313. .catch((e) => {
  314. message.error(typeof e === 'string' ? e : '指令下发失败');
  315. });
  316. }
  317. onMounted(async () => {
  318. if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
  319. optionValue.value = currentRoute.value['query']['id'] as string;
  320. }
  321. await getSysDataSource();
  322. if (props.mainConfig.timer !== 0) {
  323. timer = setInterval(() => {
  324. getMonitor();
  325. }, props.mainConfig.timer || 5000);
  326. } else {
  327. getMonitor();
  328. }
  329. });
  330. onUnmounted(() => {
  331. clearInterval(timer);
  332. });
  333. </script>
  334. <style lang="less" scoped>
  335. @import '/@/design/vent/modal.less';
  336. @ventSpace: zxm;
  337. .scene-box {
  338. --image-tab-group-bg: url('/@/assets/images/vent/tab-group-bg.png');
  339. margin-top: 30px;
  340. pointer-events: none;
  341. .history-group {
  342. margin-top: 80px;
  343. padding: 0 10px;
  344. .history-container {
  345. pointer-events: auto;
  346. background: #6195af1a;
  347. // width: 100%;
  348. border: 1px solid #00fffd22;
  349. padding: 10px 0;
  350. box-shadow: 0 0 20px #44b4ff33 inset;
  351. }
  352. }
  353. .device-button-group {
  354. // margin: 0 20px;
  355. padding: 0 10px;
  356. display: flex;
  357. pointer-events: auto;
  358. position: relative;
  359. &::after {
  360. position: absolute;
  361. content: '';
  362. width: 100%;
  363. height: 2px;
  364. top: 30px;
  365. left: -1px;
  366. border-bottom: 1px solid #0efcff;
  367. }
  368. .device-button {
  369. padding: 4px 15px;
  370. position: relative;
  371. display: flex;
  372. justify-content: center;
  373. align-items: center;
  374. font-size: 14px;
  375. color: #fff;
  376. cursor: pointer;
  377. margin: 0 3px;
  378. &::before {
  379. content: '';
  380. position: absolute;
  381. top: 0;
  382. right: 0;
  383. bottom: 0;
  384. left: 0;
  385. border: 1px solid #6176af;
  386. transform: skewX(-38deg);
  387. background-color: rgba(0, 77, 103, 85%);
  388. z-index: -1;
  389. }
  390. }
  391. .device-active {
  392. // color: #0efcff;
  393. &::before {
  394. border-color: #0efcff;
  395. box-shadow: 1px 1px 3px 1px #0efcff inset;
  396. }
  397. }
  398. }
  399. }
  400. .center-container {
  401. width: 100%;
  402. height: calc(100% - 150px);
  403. }
  404. .input-box {
  405. display: flex;
  406. align-items: center;
  407. padding-left: 10px;
  408. .input-title {
  409. color: #73e8fe;
  410. width: auto;
  411. }
  412. .@{ventSpace}-input-number {
  413. border-color: #ffffff88;
  414. }
  415. margin-right: 10px;
  416. }
  417. .button-wrapper {
  418. position: relative;
  419. top: 30px;
  420. left: 500px;
  421. display: flex;
  422. justify-content: flex-start;
  423. align-items: center;
  424. margin-top: 10px;
  425. width: 1165px !important;
  426. height: 75px;
  427. background: var(--image-tab-group-bg) no-repeat;
  428. background-image: 100% 100%;
  429. padding: 0 30px;
  430. z-index: 10;
  431. .button-box {
  432. margin: 0 10px;
  433. }
  434. }
  435. .button-box {
  436. position: relative;
  437. padding: 5px;
  438. border-radius: 5px;
  439. width: auto;
  440. height: 34px;
  441. border: 1px solid var(--vent-base-border);
  442. display: flex;
  443. align-items: center;
  444. justify-content: center;
  445. color: var(--vent-font-color);
  446. padding: 0 10px;
  447. cursor: pointer;
  448. pointer-events: all;
  449. &:hover {
  450. background: var(--vent-device-manager-control-btn-hover);
  451. }
  452. &::before {
  453. width: calc(100% - 6px);
  454. height: 26px;
  455. content: '';
  456. position: absolute;
  457. top: 3px;
  458. right: 0;
  459. left: 3px;
  460. bottom: 0;
  461. z-index: -1;
  462. border-radius: inherit;
  463. /*important*/
  464. background: var(--vent-device-manager-control-btn);
  465. }
  466. }
  467. .button-box-disable {
  468. cursor: not-allowed;
  469. border: 1px solid var(--vent-base-border);
  470. &:hover {
  471. background: none;
  472. }
  473. &:before {
  474. background: linear-gradient(#5897c299, #4a92a899);
  475. }
  476. }
  477. </style>