SimulateDetail.js 18 KB

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