SimulateDetail.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. import TabsContent from '@/components/TabsContent';
  2. import {
  3. queryBackwash,
  4. queryBackwashList,
  5. queryDesignNob,
  6. queryDesignNobList,
  7. queryDesignWash,
  8. queryDesignWashList,
  9. queryDrug,
  10. queryDrugList,
  11. queryMembrane,
  12. queryMembraneConditions,
  13. queryMembraneList,
  14. queryProjectConfig,
  15. } from '@/services/SmartOps';
  16. import { AreaChartOutlined } from '@ant-design/icons';
  17. import { useRequest } from '@umijs/max';
  18. import { DatePicker, Modal, Select, Spin } from 'antd';
  19. import dayjs from 'dayjs';
  20. import * as echarts from 'echarts';
  21. import { useEffect, useRef, useState } from 'react';
  22. import styles from './SimulateDetail.less';
  23. const { RangePicker } = DatePicker;
  24. const TYPE = {
  25. td_uf: {
  26. name: '超滤膜',
  27. device: (params) => queryMembraneList({ ...params, type: 'uf' }),
  28. chart: (params) => queryMembrane({ ...params, type: 'uf' }),
  29. },
  30. td_mf: {
  31. name: '微滤膜',
  32. device: (params) => queryMembraneList({ ...params, type: 'mf' }),
  33. chart: (params) => queryMembrane({ ...params, type: 'mf' }),
  34. },
  35. td_nf: {
  36. name: '纳滤膜',
  37. device: (params) => queryMembraneList({ ...params, type: 'nf' }),
  38. chart: (params) => queryMembrane({ ...params, type: 'nf' }),
  39. },
  40. td_ro: {
  41. name: '反渗透膜',
  42. device: (params) => queryMembraneList({ ...params, type: 'ro' }),
  43. chart: (params) => queryMembrane({ ...params, type: 'ro' }),
  44. },
  45. tdr_pac: {
  46. name: '絮凝剂投加',
  47. device: (params) => queryDrugList({ ...params, type: 'pac' }),
  48. chart: (params) => queryDrug({ ...params, type: 'pac' }),
  49. },
  50. tdr_hci: {
  51. name: 'HCI投加',
  52. device: (params) => queryDrugList({ ...params, type: 'hci' }),
  53. chart: (params) => queryDrug({ ...params, type: 'hci' }),
  54. },
  55. tdr_nob: {
  56. name: '非氧化杀菌剂投加',
  57. device: (params) => queryDrugList({ ...params, type: 'nob' }),
  58. chart: (params) => queryDrug({ ...params, type: 'nob' }),
  59. },
  60. tt_backwash: {
  61. name: '反冲洗记录',
  62. device: queryBackwashList,
  63. chart: queryBackwash,
  64. },
  65. tt_wash: {
  66. name: '大水量冲洗记录',
  67. device: queryDesignWashList,
  68. chart: queryDesignWash,
  69. },
  70. tt_nob: {
  71. name: '非氧化杀菌记录',
  72. device: queryDesignNobList,
  73. chart: queryDesignNob,
  74. },
  75. };
  76. const SimulateDetail = (props) => {
  77. const { projectId } = props;
  78. const [active, setActive] = useState();
  79. const [current, setCurrent] = useState();
  80. const { data } = useRequest(queryProjectConfig, {
  81. defaultParams: [projectId],
  82. onSuccess(data) {
  83. setActive(data[0]);
  84. },
  85. });
  86. return (
  87. <div>
  88. {data && (
  89. <TabsContent
  90. defaultActiveKey={data[0]}
  91. items={data.map((item) => ({
  92. label: TYPE[item]?.name,
  93. key: item,
  94. children: <div></div>,
  95. }))}
  96. onChange={(value) => {
  97. setActive(value);
  98. setCurrent(null);
  99. }}
  100. ></TabsContent>
  101. )}
  102. <div className={`${styles.box} card-box`}>
  103. <ChartContent
  104. active={active}
  105. projectId={projectId}
  106. current={current}
  107. setCurrent={setCurrent}
  108. />
  109. </div>
  110. </div>
  111. );
  112. };
  113. const DateTab = [
  114. {
  115. value: 'day',
  116. label: '今日',
  117. },
  118. {
  119. value: 'week',
  120. label: '本周',
  121. },
  122. {
  123. value: 'month',
  124. label: '本月',
  125. },
  126. ];
  127. const ChartContent = (props) => {
  128. const { current, projectId, active, setCurrent } = props;
  129. const [visible, setVisible] = useState(false);
  130. const [params, setParams] = useState(null);
  131. const [dateActive, setDateActive] = useState('day');
  132. const [time, setTime] = useState([dayjs().startOf('day'), dayjs()]);
  133. const timerRef = useRef({
  134. s_time: time[0].format('YYYY-MM-DD HH:mm:ss'),
  135. e_time: time[1].format('YYYY-MM-DD HH:mm:ss'),
  136. });
  137. const domRef = useRef(null);
  138. const chartRef = useRef(null);
  139. const { data, run, loading } = useRequest(
  140. () => {
  141. let params = {
  142. device_code: current.device_code,
  143. page: 1,
  144. page_size: 9999,
  145. project_id: projectId,
  146. ...timerRef.current,
  147. };
  148. setParams(params);
  149. return TYPE[active].chart(params);
  150. },
  151. {
  152. manual: true,
  153. onSuccess(data) {
  154. chartRef.current.clear();
  155. let options = getOption(data.list, active);
  156. chartRef.current.setOption(options, true);
  157. chartRef.current.resize();
  158. },
  159. },
  160. );
  161. const searchTime = (type) => {
  162. setDateActive(type);
  163. let time = [dayjs().startOf(type), dayjs()];
  164. onSearch?.(time);
  165. };
  166. const onSearch = (time) => {
  167. if (time && time.length == 2) {
  168. let s_time, e_time;
  169. s_time = time[0].format('YYYY-MM-DD HH:mm:ss');
  170. e_time = time[1].format('YYYY-MM-DD HH:mm:ss');
  171. timerRef.current = { s_time, e_time };
  172. setTime(time);
  173. run();
  174. }
  175. };
  176. useEffect(() => {
  177. chartRef.current = echarts.init(domRef.current);
  178. return () => {
  179. chartRef.current.dispose();
  180. };
  181. }, []);
  182. useEffect(() => {
  183. if (current?.device_code && active) {
  184. run();
  185. } else {
  186. chartRef.current.clear();
  187. }
  188. }, [current, active]);
  189. return (
  190. <div className={styles.chartBox}>
  191. <div className={styles.chartBoxTop}>
  192. <DeviceList
  193. active={active}
  194. projectId={projectId}
  195. current={current}
  196. onClick={setCurrent}
  197. />
  198. <div style={{ display: 'flex' }}>
  199. <div className={styles.dateTabs}>
  200. {DateTab.map((item) => (
  201. <div
  202. key={item.value}
  203. className={`${styles.dateTabsItem} ${
  204. item.value == dateActive ? styles.active : ''
  205. }`}
  206. onClick={() => searchTime(item.value)}
  207. >
  208. {item.label}
  209. </div>
  210. ))}
  211. </div>
  212. {active == 1 && (
  213. <AreaChartOutlined
  214. style={{ float: 'right', lineHeight: '56px', marginRight: 20 }}
  215. onClick={() => setVisible(true)}
  216. />
  217. )}
  218. </div>
  219. </div>
  220. <Spin spinning={loading}>
  221. <div ref={domRef} style={{ height: '30vh' }} />
  222. </Spin>
  223. <Optimization data={data?.list?.[0]} />
  224. <MembraneModal
  225. visible={visible}
  226. onCancel={() => setVisible(false)}
  227. params={params}
  228. />
  229. </div>
  230. );
  231. };
  232. const DeviceList = (props) => {
  233. const { current, onClick, projectId, active } = props;
  234. const { data, loading, run } = useRequest(
  235. () => {
  236. let params = {
  237. page: 1,
  238. page_size: 99999,
  239. project_id: projectId,
  240. };
  241. return TYPE[active]?.device(params);
  242. },
  243. {
  244. manual: true,
  245. onSuccess(data) {
  246. if (data.list?.[0]) {
  247. onClick(data.list[0]);
  248. } else {
  249. onClick(null);
  250. }
  251. },
  252. },
  253. );
  254. const handleChange = (value) => {
  255. let current = data.list.find((item) => item.device_code == value);
  256. onClick(current);
  257. };
  258. useEffect(() => {
  259. if (active) run();
  260. }, [active]);
  261. return (
  262. <Select
  263. style={{ width: 200 }}
  264. onChange={handleChange}
  265. value={current?.device_code}
  266. >
  267. {data?.list.map((item) => (
  268. <Option key={item.device_code}>{item.device_code}</Option>
  269. ))}
  270. </Select>
  271. );
  272. };
  273. function getOption(data = [], active) {
  274. let formatter,
  275. yAxisName = '',
  276. series = [],
  277. xAxis = [];
  278. var data1 = [],
  279. data2 = [],
  280. data3 = [],
  281. data4 = [];
  282. switch (active) {
  283. case 'tt_backwash':
  284. yAxisName = '清洗周期/Min';
  285. formatter = (params) => {
  286. let item = data[params[0].dataIndex];
  287. let content = '';
  288. if (item.bw_type == 1) {
  289. // PEB
  290. content += 'PEB 反洗开始时间:' + item.peb_st;
  291. content += '<br />PEB 反洗结束时间:' + item.peb_et;
  292. } else {
  293. // CEB
  294. content += 'CEB 反洗开始时间:' + item.ceb_st;
  295. content += '<br />CEB 反洗结束时间:' + item.ceb_et;
  296. content += '<br />CEB 清洗剂浓度' + item.ceb_ppm;
  297. }
  298. return content;
  299. };
  300. data?.forEach((item) => {
  301. if (item.bw_type == 1) {
  302. // 实际冲洗
  303. data1.push(Math.ceil(item.peb_interval / 60));
  304. // TODO:冲洗
  305. data2.push(Math.ceil(item.peb_interval / 60));
  306. data3.push(null);
  307. data4.push(null);
  308. xAxis.push(dayjs(item.peb_st).format('YYYY-MM-DD HH:mm:ss'));
  309. } else {
  310. data1.push(null);
  311. data2.push(null);
  312. data3.push(Math.ceil(item.peb_interval / 60));
  313. // TODO:模拟冲洗
  314. data4.push(Math.ceil(item.peb_interval / 60));
  315. xAxis.push(dayjs(item.ceb_st).format('YYYY-MM-DD HH:mm:ss'));
  316. }
  317. });
  318. series = [
  319. {
  320. name: '物理实际冲洗',
  321. type: 'bar',
  322. barMaxWidth: '20px',
  323. data: data1,
  324. },
  325. {
  326. name: '物理模拟冲洗',
  327. type: 'bar',
  328. barMaxWidth: '20px',
  329. data: data2,
  330. },
  331. {
  332. name: '化学实际冲洗',
  333. type: 'bar',
  334. barMaxWidth: '20px',
  335. data: data3,
  336. },
  337. {
  338. name: '化学模拟冲洗',
  339. type: 'bar',
  340. barMaxWidth: '20px',
  341. data: data4,
  342. },
  343. ];
  344. break;
  345. case 'tt_wash':
  346. yAxisName = '清洗周期/Min';
  347. formatter = (params) => {
  348. let item = data[params[0].dataIndex];
  349. let content = '';
  350. content += '反洗开始时间:' + item.st;
  351. content += '<br />反洗结束时间:' + item.et;
  352. return content;
  353. };
  354. data?.forEach((item) => {
  355. // 实际冲洗
  356. data1.push(Math.ceil(item.interval / 60));
  357. // TODO:模拟冲洗
  358. data2.push(Math.ceil(item.interval / 60));
  359. xAxis.push(dayjs(item.st).format('YYYY-MM-DD HH:mm:ss'));
  360. });
  361. series = [
  362. {
  363. name: '实际冲洗',
  364. type: 'bar',
  365. barMaxWidth: '20px',
  366. data: data1,
  367. },
  368. {
  369. name: '模拟冲洗',
  370. type: 'bar',
  371. barMaxWidth: '20px',
  372. data: data2,
  373. },
  374. ];
  375. break;
  376. case 'tt_nob':
  377. yAxisName = '杀菌周期/Min';
  378. formatter = (params) => {
  379. let item = data[params[0].dataIndex];
  380. let content = '';
  381. content += '杀菌开始时间:' + (item.st || '-');
  382. content += '<br />杀菌结束时间:' + (item.et || '-');
  383. return content;
  384. };
  385. var data1 = [],
  386. data2 = [];
  387. data?.forEach((item) => {
  388. // 实际冲洗
  389. data1.push(Math.ceil(item.interval / 60));
  390. // TODO:模拟冲洗
  391. data2.push(Math.ceil(item.interval / 60));
  392. xAxis.push(dayjs(item.st).format('YYYY-MM-DD HH:mm:ss'));
  393. });
  394. series = [
  395. {
  396. name: '实际',
  397. type: 'bar',
  398. barMaxWidth: '20px',
  399. data: data1,
  400. },
  401. {
  402. name: '模拟',
  403. type: 'bar',
  404. barMaxWidth: '20px',
  405. data: data2,
  406. },
  407. ];
  408. break;
  409. case 'tdr_hci':
  410. case 'tdr_nob':
  411. case 'tdr_pac':
  412. yAxisName = '投加量';
  413. // formatter = params => {
  414. // let content = '';
  415. // let item = data[params[0].dataIndex];
  416. // content += item.c_time;
  417. // content += '<br />最高加药浓度' + item.dosh + 'g/m3';
  418. // content += '<br />最低加药浓度:' + item.dosl + 'g/m3';
  419. // content += '<br />最高加药浊度:' + item.tubh + 'g/m3';
  420. // content += '<br />最低加药浊度:' + item.tubl + 'g/m3';
  421. // content += '<br />实际进水浊度:' + item.tubr;
  422. // return content;
  423. // };
  424. data?.forEach((item) => {
  425. // 实际冲洗
  426. data1.push(Math.ceil(item.fr / 60));
  427. // TODO:模拟冲洗
  428. data2.push(Math.ceil(item.fcoa / 60));
  429. xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
  430. });
  431. series = [
  432. {
  433. name: '实际物理投加量',
  434. type: 'bar',
  435. barMaxWidth: '20px',
  436. data: data1,
  437. },
  438. {
  439. name: '理论物理投加量',
  440. type: 'bar',
  441. barMaxWidth: '20px',
  442. data: data2,
  443. },
  444. ];
  445. break;
  446. case 'td_uf':
  447. case 'td_mf':
  448. case 'td_nf':
  449. yAxisName = '渗透率';
  450. data?.forEach((item) => {
  451. // 实际跨膜压差
  452. data1.push(item.permeability);
  453. // 模拟跨膜压差
  454. data2.push(item.std_permeability);
  455. xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
  456. });
  457. series = [
  458. {
  459. name: '实际渗透率',
  460. type: 'line',
  461. data: data1,
  462. showSymbol: false,
  463. },
  464. {
  465. name: '模拟渗透率',
  466. type: 'line',
  467. data: data2,
  468. showSymbol: false,
  469. },
  470. ];
  471. break;
  472. case 'td_ro':
  473. yAxisName = '跨膜压差';
  474. data?.forEach((item) => {
  475. // 实际跨膜压差
  476. data1.push(item.extend['1st_Stage_DP']);
  477. data2.push(item.extend['2nd_Stage_DP']);
  478. // 模拟跨膜压差
  479. data3.push(item.stabilize_extend['1st_Stage_DP']);
  480. data4.push(item.stabilize_extend['2nd_Stage_DP']);
  481. xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
  482. });
  483. series = [
  484. {
  485. name: '实际一段跨膜压差',
  486. type: 'line',
  487. data: data1,
  488. showSymbol: false,
  489. },
  490. {
  491. name: '实际二段跨膜压差',
  492. type: 'line',
  493. data: data2,
  494. showSymbol: false,
  495. },
  496. {
  497. name: '模拟一段跨膜压差',
  498. type: 'line',
  499. data: data3,
  500. showSymbol: false,
  501. },
  502. {
  503. name: '模拟二段跨膜压差',
  504. type: 'line',
  505. data: data4,
  506. showSymbol: false,
  507. },
  508. ];
  509. break;
  510. }
  511. const option = {
  512. color: ['#FFC800', '#30EDFD', '#4096ff', '#ff4d4f', '#ffa940'],
  513. tooltip: {
  514. trigger: 'axis',
  515. axisPointer: {
  516. type: 'shadow',
  517. },
  518. formatter,
  519. },
  520. legend: {
  521. textStyle: {
  522. // color: '#fff',
  523. fontSize: 18,
  524. },
  525. },
  526. grid: {
  527. left: '3%',
  528. right: '4%',
  529. bottom: '3%',
  530. containLabel: true,
  531. },
  532. xAxis: {
  533. type: 'category',
  534. data: xAxis,
  535. axisLine: {
  536. lineStyle: {
  537. // color: '#fff',
  538. },
  539. },
  540. splitLine: {
  541. lineStyle: {
  542. // color: '#fff',
  543. },
  544. },
  545. axisLabel: {
  546. // color: '#fff',
  547. },
  548. },
  549. yAxis: {
  550. name: yAxisName,
  551. type: 'value',
  552. boundaryGap: [0, 0.01],
  553. axisLine: {
  554. lineStyle: {
  555. // color: '#fff',
  556. },
  557. },
  558. splitLine: {
  559. lineStyle: {
  560. // color: '#fff',
  561. },
  562. },
  563. axisLabel: {
  564. // color: '#fff',
  565. },
  566. },
  567. series,
  568. };
  569. return option;
  570. }
  571. const MembraneModal = (props) => {
  572. const { visible, onCancel, params } = props;
  573. const domRef = useRef(null);
  574. const chartRef = useRef(null);
  575. const { run, loading } = useRequest(queryMembraneConditions, {
  576. manual: true,
  577. onSuccess(data) {
  578. console.log(data);
  579. let options = getMembraneOption(data.list);
  580. chartRef.current.setOption(options, true);
  581. },
  582. });
  583. useEffect(() => {
  584. if (visible) {
  585. if (!chartRef.current) {
  586. chartRef.current = echarts.init(document.getElementById('chart'));
  587. run(params);
  588. }
  589. }
  590. }, [visible]);
  591. return (
  592. <Modal
  593. forceRender
  594. title="渗透率图表"
  595. open={visible}
  596. onCancel={onCancel}
  597. footer={null}
  598. >
  599. <Spin spinning={loading}>
  600. <div id="chart" style={{ height: '60vh' }} />
  601. </Spin>
  602. </Modal>
  603. );
  604. };
  605. function getMembraneOption(data = []) {
  606. const option = {
  607. color: ['#FFC800', '#30EDFD', '#4096ff', '#ff4d4f', '#ffa940'],
  608. tooltip: {
  609. trigger: 'axis',
  610. axisPointer: {
  611. type: 'shadow',
  612. },
  613. },
  614. legend: {
  615. textStyle: {
  616. // color: '#fff',
  617. fontSize: 18,
  618. },
  619. },
  620. grid: {
  621. left: '3%',
  622. right: '4%',
  623. bottom: '3%',
  624. containLabel: true,
  625. },
  626. xAxis: {
  627. type: 'category',
  628. data: data.map((item) => item.c_time),
  629. axisLine: {
  630. lineStyle: {
  631. // color: '#fff',
  632. },
  633. },
  634. splitLine: {
  635. lineStyle: {
  636. // color: '#fff',
  637. },
  638. },
  639. axisLabel: {
  640. // color: '#fff',
  641. },
  642. },
  643. yAxis: {
  644. name: '渗透率',
  645. type: 'value',
  646. boundaryGap: [0, 0.01],
  647. axisLine: {
  648. lineStyle: {
  649. // color: '#fff',
  650. },
  651. },
  652. splitLine: {
  653. lineStyle: {
  654. // color: '#fff',
  655. },
  656. },
  657. axisLabel: {
  658. // color: '#fff',
  659. },
  660. },
  661. series: [
  662. {
  663. type: 'line',
  664. showSymbol: false,
  665. areaStyle: {
  666. opacity: 0.1,
  667. },
  668. type: 'line',
  669. smooth: true,
  670. data: data.map((v) => v.permeability),
  671. },
  672. ],
  673. };
  674. return option;
  675. }
  676. const Optimization = ({ data }) => {
  677. if (!data?.optimization) return '';
  678. return (
  679. <div className={styles.optimization}>
  680. <div className={styles.title1}>
  681. {dayjs(data.c_time).format('YYYY-MM-DD HH:mm')}
  682. </div>
  683. <div className={styles.title2}>调整内容</div>
  684. {Object.entries(data.optimization).map(([key, item]) => (
  685. <div className={styles.content}>
  686. {key}:{item.remark}
  687. </div>
  688. ))}
  689. </div>
  690. );
  691. };
  692. export default SimulateDetail;