MedicinalView.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. <script setup>
  2. import { ref, onMounted, computed, onUnmounted } from 'vue';
  3. import {useMessage, NProgress } from 'naive-ui';
  4. import { TheChatView } from '@/components';
  5. import { controlApi } from "@/api/control";
  6. import BaseTitle from './components/BaseTitle.vue';
  7. import NumberPanel from './NumberPanel.vue';
  8. import CirclePanel from './CirclePanel.vue';
  9. import EchartLeft from './EchartLeft.vue'
  10. import EchartRight from './EchartRight.vue'
  11. import DrawerSetting from './DrawerSetting.vue';
  12. import ParamterCard from './ParamterCard.vue';
  13. import DrawerWarning from './DrawerWarning.vue';
  14. import dayjs from 'dayjs';
  15. let isProcessing = false;
  16. const lastDataSource = ref({});
  17. const warningCollectList = ref([]);
  18. const updateTime = ref(dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"));
  19. const message = useMessage();
  20. const processNumber = ref(0);
  21. const systemStatus = ref(0);
  22. const configurationStatus = ref(1);
  23. const timerPanel = ref(null);
  24. const timerParams = ref(null);
  25. // 中间面板数值
  26. const panelResultData = ref({});
  27. const paramterDrawerVisible = ref(false);
  28. const warningDrawerVisible = ref(false);
  29. const paramterValue = ref({});
  30. const systemSwitchType = computed(() => configurationStatus.value === 0 && systemStatus.value === 1);
  31. // 开始投放
  32. const handleSystemStatus = () => {
  33. if ( isProcessing ) return;
  34. isProcessing = true;
  35. if ( configurationStatus.value == 1 ) {
  36. return message.warning('当前组态未启用,无法投放');
  37. }
  38. const addStatus = systemStatus.value === 0 ? 1 : 0;
  39. controlApi.putSystemStatus({ addStatus });
  40. operateNumbers(addStatus === 0 ? "decrement" : "increment", () => {
  41. systemStatus.value = addStatus;
  42. isProcessing = false
  43. message.warning(addStatus === 0 ? '当前投药状态:已停用' : '当前投药状态:投放中');
  44. })
  45. }
  46. // 更新参数设置
  47. const handleUpdateParams = () => {
  48. paramterDrawerVisible.value = false;
  49. initData();
  50. initPanelData();
  51. }
  52. const getNumVal = (type, n1, n2) => {
  53. if ( type == 0 ) {
  54. return Math.max(n1, n2);
  55. }
  56. if ( type == 1 ) {
  57. return n1 || n1 == 0 ? Number(n1.toFixed(2)) : '--'
  58. }
  59. if ( type == 2 ) {
  60. return n2 || n2 == 0 ? Number(n2.toFixed(2)) : '--'
  61. }
  62. }
  63. const getCurrentTypeData = () => {
  64. const {
  65. type,
  66. hycXsyOne, hycXsyTwo,
  67. qycYxyOne, qycYxyTwo, qycAdOne, qycAdTwo,
  68. htfksd,
  69. } = lastDataSource.value;
  70. if ( type == 0 ) {
  71. return {
  72. type,
  73. hycXsy: Math.max(hycXsyOne, hycXsyTwo),
  74. qycYxy: Math.max(qycYxyOne, qycYxyTwo),
  75. qycAd: Math.max(qycAdOne, qycAdTwo),
  76. htfksd,
  77. }
  78. }
  79. if (type == 1) {
  80. return {
  81. type,
  82. hycXsy: hycXsyOne,
  83. qycYxy: qycYxyOne,
  84. qycAd: qycAdOne,
  85. htfksd
  86. }
  87. }
  88. if (type == 2) {
  89. return {
  90. type,
  91. hycXsy: hycXsyTwo,
  92. qycYxy: qycYxyTwo,
  93. qycAd: qycAdTwo,
  94. htfksd
  95. }
  96. }
  97. }
  98. const operateNumbers = (operationType, callBack) => {
  99. const totalTime = 3000;
  100. const steps = 100;
  101. const intervalTime = totalTime / steps;
  102. let currentNumber = operationType === 'decrement' ? 100 : 1;
  103. processNumber.value = currentNumber;
  104. const intervalId = setInterval(() => {
  105. operationType === 'increment' ? currentNumber++ : currentNumber--
  106. processNumber.value = currentNumber
  107. if ((operationType === 'increment' && currentNumber > 100) ||
  108. (operationType === 'decrement' && currentNumber < 0)) {
  109. clearInterval(intervalId);
  110. callBack && callBack();
  111. }
  112. }, intervalTime);
  113. }
  114. const isEmpty = (val) => {
  115. return !(val === null || val === undefined || val === '')
  116. }
  117. const handleWarningDraw = () => {
  118. if ( !warningCollectList.value.length ) {
  119. return message.warning('当前没有系统告警');
  120. }
  121. warningDrawerVisible.value = true;
  122. }
  123. const getWarningInfo = async () => {
  124. const { data } = await controlApi.getWarningParams();
  125. const { kzmbplbjz, hycxsygkz, xhycbjz, jylpybjz, minAddAmount } = data;
  126. const {
  127. hycXsy,
  128. qycYxy,
  129. qycAd,
  130. htfksd,
  131. // 系统加药量
  132. medicineAmount,
  133. } = getCurrentTypeData();
  134. if ( isEmpty(hycXsy) && isEmpty(htfksd) && isEmpty(kzmbplbjz)) {
  135. const content = [];
  136. if (( hycXsy - htfksd ) > kzmbplbjz ) {
  137. content.push('• 好氧池硝酸盐控制目标偏移过大')
  138. }
  139. if ( hycXsy > hycxsygkz ) {
  140. content.push('• 好氧池硝酸盐超管控值')
  141. }
  142. if (content.length) {
  143. warningCollectList.value.push({
  144. title: '反硝化异常报警',
  145. content,
  146. desc: '请排查现场工况/调整控制参数,非碳源量的问题,建议切换手动控制'
  147. })
  148. }
  149. }
  150. if (isEmpty(qycYxy) && isEmpty(qycAd) && isEmpty(hycXsy) && isEmpty(xhycbjz)) {
  151. if ((qycYxy + qycAd - hycXsy) > xhycbjz) {
  152. warningCollectList.value.push({
  153. title: '硝化异常报警',
  154. content: [],
  155. desc: '请排查进水水质、曝气系统、活性污泥系统等,请切手动运行'
  156. })
  157. }
  158. }
  159. if (isEmpty(medicineAmount) && isEmpty(minAddAmount) && isEmpty(jylpybjz)) {
  160. if ( medicineAmount > minAddAmount && medicineAmount > jylpybjz) {
  161. warningCollectList.value.push({
  162. title: '加药量偏移报警',
  163. content: ['• 系统计算加药与现场实际流量计偏移过大'],
  164. desc: '请排查现场碳源储罐液位、加药泵和流量计等,确保运行正常'
  165. })
  166. }
  167. }
  168. }
  169. const initPanelData = async () => {
  170. const { data } = await controlApi.getLastResult();
  171. panelResultData.value = data;
  172. updateTime.value = dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss");
  173. }
  174. const initData = async () => {
  175. const { data } = await controlApi.getBaseData();
  176. const {
  177. addType,
  178. numberBeng, type,
  179. jsLlOne, jsLlTwo,
  180. jsCodOne, jsCodTwo,
  181. jsTnOne, jsTnTwo,
  182. hycXsyOne, hycXsyTwo,
  183. qycYxyOne, qycYxyTwo,
  184. qycAdOne, qycAdTwo,
  185. // 系统加药量
  186. medicineAmount,
  187. // 瞬时加药量
  188. tytjTransientLL
  189. } = data;
  190. const numberBengEunm = {
  191. 0: '1号加药泵',
  192. 1: '2号加药泵',
  193. 2: '3号加药泵'
  194. }
  195. const typeEnum = {
  196. 0: '自动',
  197. 1: '1号池',
  198. 2: '2号池',
  199. 3: '手动'
  200. }
  201. paramterValue.value = {
  202. device: `${numberBengEunm[numberBeng]} / ${typeEnum[type]}`,
  203. jsll: getNumVal(type, jsLlOne, jsLlTwo) + ' m³/h',
  204. cod: getNumVal(type, jsCodOne, jsCodTwo) + ' mg/L',
  205. jszd: getNumVal(type, jsTnOne, jsTnTwo) + ' mg/L',
  206. hycxsy: getNumVal(type, hycXsyOne, hycXsyTwo) + ' mg/L',
  207. qycxsy: getNumVal(type, qycYxyOne, qycYxyTwo) + ' mg/L',
  208. qycan: getNumVal(type, qycAdOne, qycAdTwo) + ' mg/L',
  209. };
  210. configurationStatus.value = addType;
  211. lastDataSource.value = data;
  212. }
  213. onMounted(async () => {
  214. // 初始化
  215. await initData();
  216. // 投药量数据
  217. initPanelData();
  218. // 获取报警相关数据
  219. getWarningInfo();
  220. // 获取是否允许投药开关
  221. await controlApi.getSystemStatus().then(({ data }) => {
  222. // 0不允许 1允许
  223. systemStatus.value = data;
  224. });
  225. processNumber.value = systemSwitchType.value ? 100 : 0;
  226. timerPanel.value = setInterval(initPanelData, 5 * 60 * 1000)
  227. timerParams.value = setInterval(initData, 40 * 60 * 1000);
  228. })
  229. onUnmounted(() => {
  230. clearInterval(timerPanel);
  231. clearInterval(timerParams);
  232. })
  233. </script>
  234. <template>
  235. <section class="flex items-start h-full">
  236. <TheChatView leftTitle="智适应碳源投加" :isChatSlot="false" :isFooter="false">
  237. <template #control>
  238. <div class="control-container">
  239. <div class="arg-section">
  240. <div class="left-card space-y-[16px]">
  241. <BaseTitle title="智能投加计算" type="1"></BaseTitle>
  242. <ParamterCard :data="paramterValue"></ParamterCard>
  243. </div>
  244. <div class="right-card">
  245. <div class="header">
  246. <h4 class="title">智能系统参数</h4>
  247. <ul class="btn-list space-x-[8px]">
  248. <li class="item" @click="paramterDrawerVisible = true">
  249. <span>参数设置</span>
  250. </li>
  251. <li class="item" @click="handleWarningDraw">
  252. <span>系统告警</span>
  253. <span class="waring-circle-icon" v-show="warningCollectList.length != 0"></span>
  254. </li>
  255. </ul>
  256. </div>
  257. <div class="result-content">
  258. <div class="number_card space-x-[20px]">
  259. <NumberPanel direction="left" title="智能控制系数" :value="panelResultData.kzxs"></NumberPanel>
  260. <CirclePanel :medicineAmount="panelResultData.calculateVal" :tytjTransientLL="panelResultData.realValue"></CirclePanel>
  261. <NumberPanel direction="right" title="硝酸盐智能设定" unit="mg/L" :value="panelResultData.htfksd"></NumberPanel>
  262. </div>
  263. <div class="progress_card space-y-[8px]">
  264. <span class="time">模型更新时间: {{ updateTime }}</span>
  265. <div class="progress">
  266. <NProgress
  267. processing
  268. type="line"
  269. :border-radius="0"
  270. :percentage="processNumber"
  271. :show-indicator="false"
  272. :height="10"
  273. :color="'red'"
  274. rail-color="transparent"
  275. class="custom-progress"
  276. ></NProgress>
  277. </div>
  278. <span class="tips">LibraAI{{ systemSwitchType ? "投药中" : "未启用" }}...</span>
  279. </div>
  280. <div class="play_card">
  281. <div class="play-btn space-x-[6px]" @click="handleSystemStatus">
  282. <span :class="[systemSwitchType ? 'icon_end' : 'icon_start' ]"></span>
  283. <span>{{ systemSwitchType ? "暂停" : "开始" }}</span>
  284. </div>
  285. </div>
  286. </div>
  287. </div>
  288. </div>
  289. <div class="echart-section space-x-[20px]">
  290. <EchartLeft></EchartLeft>
  291. <EchartRight></EchartRight>
  292. </div>
  293. </div>
  294. </template>
  295. </TheChatView>
  296. </section>
  297. <!-- 参数设置 - 抽屉 -->
  298. <DrawerSetting
  299. v-model:show="paramterDrawerVisible"
  300. @on-update="handleUpdateParams"
  301. ></DrawerSetting>
  302. <!-- 系统告警 - 抽屉 -->
  303. <DrawerWarning
  304. v-model:show="warningDrawerVisible"
  305. :warning-collect-list="warningCollectList"
  306. ></DrawerWarning>
  307. </template>
  308. <style lang="scss" scoped>
  309. .control-container {
  310. height: 100%;
  311. .arg-section {
  312. @include flex(x, start, start);
  313. height: 57%;
  314. padding: 16px 14px;
  315. border: 1px solid #fff;
  316. border-radius: 10px;
  317. background: url(@/assets/images/control/bg-top.png) left center no-repeat;
  318. background-size: 878px 100% ;
  319. overflow: hidden;
  320. .left-card {
  321. flex-shrink: 0;
  322. width: 338px;
  323. height: 100%;
  324. display: flex;
  325. flex-flow: column;
  326. }
  327. .right-card {
  328. flex: 1;
  329. height: 100%;
  330. .header {
  331. height: 40px;
  332. position: relative;
  333. text-align: center;
  334. .title {
  335. color: #1A2029;
  336. font-size: 15px;
  337. font-weight: bold;
  338. line-height: 24px;
  339. }
  340. .btn-list {
  341. position: absolute;
  342. top: 0;
  343. right: 0;
  344. @include flex(x, center, center);
  345. color: #1A2029;
  346. font-size: 12px;
  347. font-weight: 400;
  348. .item {
  349. position: relative;
  350. @include flex(x, center, center);
  351. width: 68px;
  352. height: 28px;
  353. border-radius: 4px;
  354. background: #fff;
  355. cursor: pointer;
  356. .waring-circle-icon {
  357. position: absolute;
  358. top: 2px;
  359. right: 2px;
  360. width: 5px;
  361. height: 5px;
  362. border-radius: 50%;
  363. background: #FF4920;
  364. }
  365. }
  366. }
  367. }
  368. .result-content {
  369. @include flex(y, center, around);
  370. height: calc(100% - 40px);
  371. // padding: 0px 86px 0 86px;
  372. background-clip: padding-box;
  373. .number_card {
  374. display: flex;
  375. align-items: center;
  376. justify-content: center;
  377. }
  378. .progress_card {
  379. @include flex(y, center, center);
  380. padding-top: 6px;
  381. text-align: center;
  382. .time {
  383. width: 180px;
  384. color: #F6A52C;
  385. font-size: 10px;
  386. font-style: normal;
  387. font-weight: 400;
  388. line-height: 16px;
  389. }
  390. .tips {
  391. text-align: center;
  392. font-size: 12px;
  393. font-weight: 500;
  394. line-height: 24px;
  395. background: linear-gradient(90deg, #0059FF 0%, #29C2FA 100%);
  396. background-clip: text;
  397. -webkit-background-clip: text;
  398. -webkit-text-fill-color: transparent;
  399. }
  400. .progress {
  401. width: 165px;
  402. height: 10px;
  403. background: #cce7ec;
  404. box-shadow: 0px 5px 5px #ccc;
  405. }
  406. }
  407. .play_card {
  408. @include flex(x, center, center);
  409. .play-btn {
  410. @include flex(x, center, center);
  411. width: 120px;
  412. height: 48px;
  413. background: url("@/assets/images/control/bg-play-btn.png") center center no-repeat;
  414. background-size: cover;
  415. color: #2454FF;
  416. font-size: 14px;
  417. font-weight: 500;
  418. line-height: 16px;
  419. cursor: pointer;
  420. user-select: none;
  421. .icon_end, .icon_start {
  422. display: block;
  423. width: 28px;
  424. height: 28px;
  425. }
  426. .icon_end {
  427. background: url("@/assets/images/control/icon-end.svg") center center no-repeat;
  428. background-size: cover;
  429. }
  430. .icon_start {
  431. background: url("@/assets/images/control/icon-start.svg") center center no-repeat;
  432. background-size: cover;
  433. }
  434. }
  435. }
  436. }
  437. }
  438. }
  439. .echart-section {
  440. @include flex(x, start, start);
  441. height: 43%;
  442. padding-top: 16px;
  443. }
  444. }
  445. </style>
  446. <style lang="scss">
  447. .progress {
  448. .custom-progress {
  449. .n-progress-graph-line-fill {
  450. background: linear-gradient(to right, rgba(0, 101, 253, 1), rgba(0, 239, 234, 1)) !important;
  451. }
  452. }
  453. }
  454. // .custom-tab_item {
  455. // @include flex (x, center, center);
  456. // height: 35px;
  457. // border-radius: 4px;
  458. // background: #F3F5FA;
  459. // &.n-tabs-tab--active {
  460. // transition: none !important;
  461. // border-radius: 4px;
  462. // transition: none !important;
  463. // background: url('https://static.fuxicarbon.com/bigModel/pc/tab-border-item-2x.png') -3px 0px no-repeat, linear-gradient(180deg, #F1F3FE 0%, #FFF 100%);
  464. // background-size: 107% 100%;
  465. // }
  466. // }
  467. .control-container .left-section {
  468. .n-tabs-nav-scroll-content {
  469. padding-bottom: 10px;
  470. }
  471. }
  472. </style>