Quellcode durchsuchen

merge from origin/develop

ZhaoJun vor 1 Jahr
Ursprung
Commit
23f783743d
42 geänderte Dateien mit 6103 neuen und 58 gelöschten Zeilen
  1. 31 0
      .umirc.ts
  2. 4 2
      package.json
  3. 154 0
      src/components/ThresholdDetail/NewNumberBar.js
  4. 226 0
      src/components/ThresholdDetail/NumberBar.js
  5. 178 0
      src/components/ThresholdDetail/ThresholdModal.js
  6. 30 0
      src/components/ThresholdDetail/index.js
  7. 96 0
      src/components/ThresholdDetail/index.less
  8. 46 0
      src/pages/EqSelfInspection/ReportDetail.js
  9. 409 0
      src/pages/EqSelfInspection/Statistics.js
  10. 932 0
      src/pages/EqSelfInspection/components/Detail.js
  11. 75 0
      src/pages/EqSelfInspection/components/PatrolReportDetail.less
  12. 317 0
      src/pages/EqSelfInspection/index.js
  13. 146 0
      src/pages/EqSelfInspection/index.less
  14. 273 0
      src/pages/EqSelfInspection/models/eqSelfInspection.js
  15. 229 0
      src/pages/Smart/ConditionDetection.js
  16. 75 0
      src/pages/Smart/ConditionDetection.less
  17. 220 0
      src/pages/Smart/OptimizationTasks.js
  18. 15 0
      src/pages/Smart/OptimizationTasks.less
  19. 167 0
      src/pages/Smart/Params/Edit.js
  20. 312 0
      src/pages/Smart/Params/Mock.js
  21. 77 0
      src/pages/Smart/Params/View.js
  22. 96 0
      src/pages/Smart/Params/components/BarChart.js
  23. 79 0
      src/pages/Smart/Params/components/BasicParamsForm.js
  24. 104 0
      src/pages/Smart/Params/components/ControlParamsForm.js
  25. 137 0
      src/pages/Smart/Params/components/RadarChartModule.js
  26. 103 0
      src/pages/Smart/Params/components/TargetList.js
  27. 113 0
      src/pages/Smart/Params/components/TargetOperatingConditions.js
  28. 103 0
      src/pages/Smart/Params/components/WorkConditionAssessment.js
  29. 112 0
      src/pages/Smart/Params/index.js
  30. 9 0
      src/pages/Smart/Params/index.less
  31. 20 0
      src/pages/Smart/Simulate.js
  32. 1 1
      src/pages/Smart/components/CircleScore.less
  33. 39 0
      src/pages/Smart/components/Panel.js
  34. 783 0
      src/pages/Smart/components/SimulateDetail.js
  35. 102 0
      src/pages/Smart/components/SimulateDetail.less
  36. 117 0
      src/pages/Smart/components/SimulatePie.js
  37. 6 6
      src/pages/Smart/index.js
  38. 4 4
      src/pages/Smart/index.less
  39. 22 14
      src/services/SmartOps.js
  40. 75 0
      src/services/eqSelfInspection.js
  41. 0 21
      src/utils/utils.js
  42. 66 10
      yarn.lock

+ 31 - 0
.umirc.ts

@@ -6,6 +6,7 @@ export default defineConfig({
   model: {},
   initialState: {},
   request: {},
+  dva: {},
   layout: false,
   publicPath: process.env.NODE_ENV == 'development' ? '/' : '/gt-dig/',
   metas: [
@@ -38,6 +39,36 @@ export default defineConfig({
       path: '/smart/work/:projectId',
       component: './Smart',
     },
+    {
+      name: '工况评估',
+      path: '/smart/condition-detection/:projectId',
+      component: './Smart/ConditionDetection',
+    },
+    {
+      name: '优化任务',
+      path: '/smart/optimization-tasks/:projectId',
+      component: './Smart/OptimizationTasks',
+    },
+    {
+      name: '模拟仿真',
+      path: '/smart/simulate/:projectId',
+      component: './Smart/Simulate',
+    },
+    {
+      name: '系统自检',
+      path: '/self-inspection/:projectId',
+      component: './EqSelfInspection',
+    },
+    {
+      name: '自检报告',
+      path: '/self-inspection/detail/:projectId/:routeId',
+      component: './EqSelfInspection/ReportDetail',
+    },
+    {
+      name: '自检统计',
+      path: '/self-inspection/statistics/:projectId',
+      component: './EqSelfInspection/Statistics',
+    },
     {
       name: '任务管理',
       path: '/task-manage/:projectId',

+ 4 - 2
package.json

@@ -11,11 +11,13 @@
     "start": "npm run dev"
   },
   "dependencies": {
-    "@ant-design/icons": "^5.0.1",
+    "@ant-design/icons": "^4.7.0",
     "@ant-design/pro-components": "^2.4.4",
     "@umijs/max": "^4.0.75",
     "antd": "^5.4.0",
-    "qs": "^6.11.2"
+    "echarts": "^5.4.3",
+    "qs": "^6.11.2",
+    "react-zmage": "^0.8.5"
   },
   "devDependencies": {
     "@types/react": "^18.0.33",

+ 154 - 0
src/components/ThresholdDetail/NewNumberBar.js

@@ -0,0 +1,154 @@
+import styles from './index.less';
+
+const NewNumberBar = (props) => {
+  const { breakdown = [], exception = [], current, scale = false } = props;
+  let number = [];
+  breakdown.forEach((item) => {
+    number.push(item.ThresholdValue);
+    if (item.ThresholdValue2) number.push(item.ThresholdValue2);
+  });
+  exception.forEach((item) => {
+    number.push(item.ThresholdValue);
+    if (item.ThresholdValue2) number.push(item.ThresholdValue2);
+  });
+  let numbers = [...new Set(number)].sort((a, b) => a - b);
+  if (current) number.push(current);
+  let scaleList = [...new Set(number)].sort((a, b) => a - b);
+  const section = scaleList.length + 1; //current || current == 0 ? scaleList.length + 2 :
+
+  const getLeft = (cur) => {
+    let index = scaleList.findIndex((value) => value == cur);
+    return `${((index + 1) * 100) / section}%`;
+  };
+  const getWidth = (cur) => {
+    let index = scaleList.findIndex((value) => value == cur);
+    return `${((section - (index + 1)) * 100) / section}%`;
+  };
+
+  const getMarginLeft = (item) => {
+    const { ThresholdValue, ThresholdValue2 } = item;
+    let index = scaleList.findIndex((value) => value == ThresholdValue);
+    let index2 = scaleList.findIndex((value) => value == ThresholdValue2);
+    return `${((index2 - index) * 100) / section}%`;
+  };
+
+  const getStyle = (item) => {
+    // let [min, max] = item;
+    const { ThresholdType: type, ThresholdValue, ThresholdValue2 } = item;
+    if (type == 0) return {};
+    else if (type == 1 || type == 12)
+      // 大于、大于等于
+      return {
+        width: '100%',
+        width: getWidth(item.ThresholdValue),
+        marginLeft: getLeft(item.ThresholdValue),
+      };
+    else if (type == 2 || type == 13)
+      // 小于、小于等于
+      return {
+        width: getLeft(item.ThresholdValue),
+      };
+    else if (type == 3) {
+      // 等于
+      return { width: '1%', marginLeft: getLeft(item.ThresholdValue) };
+    }
+    // 区间内
+    else {
+      let index1 = scaleList.findIndex((value) => value == ThresholdValue);
+      let index2 = scaleList.findIndex((value) => value == ThresholdValue2);
+      return {
+        // width: ((ThresholdValue2 - ThresholdValue) / state.total) * 100 + '%',
+        width: (index2 - index1) * (100 / section) + '%',
+        marginLeft: getLeft(item.ThresholdValue),
+      };
+    }
+  };
+
+  const renderBar = (barType, item, index) => {
+    const { ThresholdType: type } = item;
+    let key = `${barType}-${index}`;
+    const backgroundColor = barType == 'exception' ? '#FFE26D' : '#D45C41';
+
+    if (type == 5 || type == 9 || type == 10 || type == 11) {
+      // 区间外
+      return (
+        <div
+          key={key}
+          className={styles.sectionOut}
+          // style={{ display: 'flex', height: '100%', width: '100%', position: 'absolute', top: 0 }}
+        >
+          <div
+            style={{
+              height: '100%',
+              backgroundColor,
+              width: getLeft(item.ThresholdValue),
+            }}
+          >
+            {/* {scaleBlack} */}
+          </div>
+          <div
+            style={{
+              height: '100%',
+              backgroundColor,
+              width: getWidth(item.ThresholdValue2),
+              marginLeft: getMarginLeft(item),
+            }}
+          >
+            {/* {scaleBlack} */}
+          </div>
+        </div>
+      );
+    } else {
+      // 其他类型
+      return (
+        <div
+          style={{
+            ...getStyle(item),
+            backgroundColor,
+            height: '100%',
+            position: 'absolute',
+            top: 0,
+          }}
+        >
+          {/* {scaleBlack} */}
+        </div>
+      );
+    }
+  };
+  let padding = scale ? 30 : 0;
+  return (
+    <>
+      <div
+        className={styles.box}
+        style={{ marginTop: padding, marginBottom: padding }}
+      >
+        {exception.map((item, index) => renderBar('exception', item, index))}
+        {breakdown.map((item, index) => renderBar('breakdown', item, index))}
+        {(current || current === 0) && (
+          <>
+            <div
+              className={`${styles.scale} ${styles.top}`}
+              style={{ left: getLeft(current) }}
+            >
+              {parseFloat(current).toFixed(2)}
+            </div>
+            <div className={styles.current} style={{ left: getLeft(current) }}>
+              <div className={styles.currentBar}></div>
+            </div>
+          </>
+        )}
+        {scale &&
+          (numbers || []).map((item) => (
+            <div
+              key={`scale-${item}`}
+              className={styles.scale}
+              style={{ left: getLeft(item) }}
+            >
+              {item}
+            </div>
+          ))}
+      </div>
+    </>
+  );
+};
+export default NewNumberBar;

+ 226 - 0
src/components/ThresholdDetail/NumberBar.js

@@ -0,0 +1,226 @@
+import { useMemo } from 'react';
+import styles from './index.less';
+/**
+ *
+ * @param {object} props
+ * @param {array[object]} props.breakdown 异常值数据范围 [{ThresholdType: 0,ThresholdValue:1,ThresholdValue2:2}]
+ * @param {array[object]} props.exception 警告值数据范围 [{ThresholdType: 0,ThresholdValue:1,ThresholdValue2:2}]
+ * @param {number} ThresholdType 数值类型 
+ *                                <Option value={0}>无</Option>
+                                  <Option value={1}>大于</Option>
+                                  <Option value={2}>小于</Option>
+                                  <Option value={3}>等于</Option>
+                                  <Option value={4}>区间</Option>
+                                  <Option value={5}>区间外</Option>
+                                  <Option value={6}>区间左包含</Option>
+                                  <Option value={7}>区间右包含</Option>
+                                  <Option value={8}>区间全包含</Option>
+                                  <Option value={9}>区间外左包含</Option>
+                                  <Option value={10}>区间外右包含</Option>
+                                  <Option value={11}>区间外全包含</Option>
+                                  <Option value={12}>大于等于</Option>
+                                  <Option value={13}>小于等于</Option>
+ * @param {number} ThresholdValue 第一个数值 
+ * @param {number} ThresholdValue2 第二个数值 
+ * @returns
+ */
+const NumberBar = (props) => {
+  const { breakdown = [], exception = [], current, scale = false } = props;
+  const state = useMemo(() => {
+    let number = [];
+    // if (current || current === 0) number.push(current);
+    breakdown.forEach((item) => {
+      number.push(item.ThresholdValue);
+      if (item.ThresholdValue2) number.push(item.ThresholdValue2);
+    });
+    exception.forEach((item) => {
+      number.push(item.ThresholdValue);
+      if (item.ThresholdValue2) number.push(item.ThresholdValue2);
+    });
+    let scaleList = [...new Set(number)].sort((a, b) => a - b);
+    // if (current || current === 0) {
+    //   // 如果current存在,则计算最大最小值时带上current
+    //   number.push(current);
+    // }
+    let max = Math.max.apply(null, number);
+    let min = Math.min.apply(null, number);
+    let total;
+    // 后续定位的计算依赖最大值和最小值
+    // 如果最大值等于最小值则定位异常
+    if (max == min) {
+      // 手动修改最小值和最大值
+      max = max * 2;
+      min = min > 0 ? 0 : min / 2;
+    }
+    total = max - min;
+
+    return {
+      total,
+      max,
+      min,
+      scaleList,
+    };
+  }, [breakdown, exception]);
+
+  // 将所有阈值放在一个数组里并排序
+  // 宽度为300%的阈值状态条分为(阈值数量+1)段
+  // 再根据阈值在数组中的顺序关联在状态条上的位置
+  const getLeft = (current) => {
+    let index = state.scaleList.findIndex((value) => value == current);
+    let number = (index + 1) * (300 / (state.scaleList.length + 1)) - 100;
+    // let number = ((current - state.min) / state.total) * 100
+    // if(number > 140) number = 140
+    // if(number < -40) number = -40
+    return number + '%';
+  };
+  const getRight = (current) => {
+    let index = state.scaleList.findIndex((value) => value == current);
+    let number = 200 - (index + 1) * (300 / (state.scaleList.length + 1));
+    // let number = ((state.max - current) / state.total) * 100;
+    // if (number > 140) number = 140;
+    // if (number < -40) number = -40;
+    return number + '%';
+  };
+
+  const getCurrent = (current) => {
+    //找到阈值列表中小于current的第一个数字的序号
+    let index, number;
+    index = state.scaleList.findIndex((value) => value == current);
+    if (index == -1) {
+      let temp = state.scaleList.filter((value) => value < current);
+      index = temp.length - 1;
+      //取对应分段的中间值
+      number = (index + 1.5) * (300 / (state.scaleList.length + 1)) - 100;
+    } else {
+      number = (index + 1) * (300 / (state.scaleList.length + 1)) - 100;
+    }
+    if (number > 142) number = 142;
+    if (number < -142) number = -142;
+    return number + '%';
+  };
+
+  const getStyle = (item) => {
+    // let [min, max] = item;
+    const { ThresholdType: type, ThresholdValue, ThresholdValue2 } = item;
+    if (type == 0) return {};
+    else if (type == 1 || type == 12)
+      // 大于、大于等于
+      return {
+        width: '300%',
+        left: getLeft(ThresholdValue),
+      };
+    else if (type == 2 || type == 13)
+      // 小于、小于等于
+      return {
+        width: '300%',
+        right: getRight(ThresholdValue),
+      };
+    else if (type == 3) {
+      // 等于
+      return { width: '1%', left: getLeft(ThresholdValue) };
+    }
+    // 区间内
+    else {
+      let index1 = state.scaleList.findIndex(
+        (value) => value == ThresholdValue,
+      );
+      let index2 = state.scaleList.findIndex(
+        (value) => value == ThresholdValue2,
+      );
+      return {
+        // width: ((ThresholdValue2 - ThresholdValue) / state.total) * 100 + '%',
+        width: (index2 - index1) * (300 / (state.scaleList.length + 1)) + '%',
+        left: getLeft(ThresholdValue),
+      };
+    }
+  };
+
+  const renderBar = (barType, item, index) => {
+    const { ThresholdType: type } = item;
+    let key = `${barType}-${index}`;
+    let className =
+      barType == 'exception' ? styles.exception : styles.breakdown;
+    // let scaleBlack = (
+    //   <>
+    //     <div className={styles.scaleBlackLeft}></div>
+    //     <div className={styles.scaleBlackRight}></div>
+    //   </>
+    // );
+    if (type == 5 || type == 9 || type == 10 || type == 11) {
+      // 区间外
+      return (
+        <div key={key}>
+          <div
+            className={className}
+            style={{
+              width: '300%',
+              right: getRight(item.ThresholdValue),
+            }}
+          >
+            {/* {scaleBlack} */}
+          </div>
+          <div
+            className={className}
+            style={{
+              width: '300%',
+              left: getLeft(item.ThresholdValue2),
+            }}
+          >
+            {/* {scaleBlack} */}
+          </div>
+        </div>
+      );
+    } else {
+      // 其他类型
+      return (
+        <div key={key} className={className} style={getStyle(item)}>
+          {/* {scaleBlack} */}
+        </div>
+      );
+    }
+  };
+
+  let padding = scale ? 30 : 0;
+
+  return (
+    <div
+      className={styles.bar}
+      style={{ paddingTop: padding, paddingBottom: padding }}
+    >
+      <div className={styles.box}>
+        <div className={styles.success}></div>
+        {exception.map((item, index) => renderBar('exception', item, index))}
+        {breakdown.map((item, index) => renderBar('breakdown', item, index))}
+        {scale &&
+          (state.scaleList || []).map((item) => (
+            <div
+              key={`scale-${item}`}
+              className={styles.scale}
+              style={{ left: getLeft(item) }}
+            >
+              {item}
+            </div>
+          ))}
+
+        {(current || current === 0) && (
+          <>
+            <div
+              className={`${styles.scale} ${styles.top}`}
+              style={{ left: getCurrent(current) }}
+            >
+              {parseFloat(current).toFixed(2)}
+            </div>
+            <div
+              className={styles.current}
+              style={{ left: getCurrent(current) }}
+            >
+              <div className={styles.currentBar}></div>
+            </div>
+          </>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default NumberBar;

+ 178 - 0
src/components/ThresholdDetail/ThresholdModal.js

@@ -0,0 +1,178 @@
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  Icon,
+  Input,
+  InputNumber,
+  Modal,
+  Row,
+  Select,
+} from 'antd';
+import { useEffect, useState } from 'react';
+const { Option } = Select;
+
+export default function ThresholdModal(props) {
+  const { data, onOk, visible, onClose, form, edit = false, loading } = props;
+  const [jsonNumThreshold, setJsonNumThreshold] = useState({});
+
+  const renderThresholdItem = (item, index, key) => {
+    // const { ThresholdType } = this.state;
+    const ThresholdType =
+      form.getFieldValue(`JsonNumThreshold[${key}][${index}].ThresholdType`) ||
+      item?.ThresholdType;
+
+    return (
+      <Col span={24}>
+        <Card
+          size="small"
+          style={{ marginBottom: 16 }}
+          title={`阈值${index + 1}`}
+          extra={
+            <Icon
+              type="delete"
+              style={{ fontSize: 20, color: '#fff' }}
+              onClick={() => deleteThreshold(index, key)}
+            />
+          }
+        >
+          <Form.Item
+            label="阈值类型"
+            name={`JsonNumThreshold[${key}][${index}].ThresholdType`}
+            initialValue={item.ThresholdType}
+            rules={[{ required: true, message: '请选择阈值类型' }]}
+          >
+            <Select disabled={!edit} style={{ width: '100%' }}>
+              <Option value={0}>无</Option>
+              <Option value={1}>大于</Option>
+              <Option value={2}>小于</Option>
+              <Option value={3}>等于</Option>
+              <Option value={4}>区间</Option>
+              <Option value={5}>区间外</Option>
+              <Option value={6}>区间左包含</Option>
+              <Option value={7}>区间右包含</Option>
+              <Option value={8}>区间全包含</Option>
+              <Option value={9}>区间外左包含</Option>
+              <Option value={10}>区间外右包含</Option>
+              <Option value={11}>区间外全包含</Option>
+              <Option value={12}>大于等于</Option>
+              <Option value={13}>小于等于</Option>
+            </Select>
+          </Form.Item>
+
+          {ThresholdType !== 0 && (
+            <Form.Item
+              label="阈值"
+              name={`JsonNumThreshold[${key}][${index}].ThresholdValue`}
+              initialValue={item.ThresholdValue}
+              rules={[{ required: true, message: '请输入阈值' }]}
+            >
+              <InputNumber
+                style={{ width: '100%' }}
+                disabled={!edit}
+                placeholder="请输入阈值"
+              />
+            </Form.Item>
+          )}
+
+          {ThresholdType >= 4 && ThresholdType < 12 && (
+            <Form.Item
+              label="阈值2"
+              name={`JsonNumThreshold[${key}][${index}].ThresholdValue2`}
+              initialValue={item.ThresholdValue2}
+              rules={[{ required: true, message: '请输入阈值' }]}
+            >
+              <InputNumber
+                style={{ width: '100%' }}
+                disabled={!edit}
+                placeholder="请输入阈值"
+              />
+            </Form.Item>
+          )}
+          <Form.Item
+            label="处理意见"
+            name={`JsonNumThreshold[${key}][${index}].ThresholdDesc`}
+            initialValue={item.ThresholdDesc}
+          >
+            <Input style={{ width: '100%' }} disabled={!edit} />
+          </Form.Item>
+        </Card>
+      </Col>
+    );
+  };
+  const addThreshold = (key) => {
+    if (!jsonNumThreshold[key]) jsonNumThreshold[key] = [];
+    jsonNumThreshold[key].push({});
+    setJsonNumThreshold({ ...jsonNumThreshold });
+  };
+  const deleteThreshold = (index, key) => {
+    jsonNumThreshold[key].splice(index, 1);
+    setJsonNumThreshold({ ...jsonNumThreshold });
+  };
+
+  const handleOk = () => {
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      onOk(fieldsValue.JsonNumThreshold);
+    });
+  };
+  useEffect(() => {
+    // console.log(data);
+    setJsonNumThreshold(JSON.parse(JSON.stringify(data || {})));
+  }, [data]);
+
+  return (
+    <Modal
+      title="阈值详情"
+      onOk={handleOk}
+      open={visible}
+      footer={!edit ? null : undefined}
+      onCancel={onClose}
+      width={800}
+      confirmLoading={loading}
+    >
+      <Form.Item
+        className="btn-item"
+        labelCol={{ span: 7 }}
+        wrapperCol={{ span: 16 }}
+        label="警告阈值范围"
+      >
+        {edit && (
+          <Button
+            style={{ marginBottom: 20 }}
+            onClick={() => addThreshold('exception')}
+          >
+            增加阈值
+          </Button>
+        )}
+        <Row gutter={16}>
+          {jsonNumThreshold.exception?.map((item, index) =>
+            renderThresholdItem(item, index, 'exception'),
+          )}
+        </Row>
+      </Form.Item>
+
+      <Form.Item
+        className="btn-item"
+        labelCol={{ span: 7 }}
+        wrapperCol={{ span: 16 }}
+        label="异常阈值范围"
+      >
+        {edit && (
+          <Button
+            style={{ marginBottom: 20 }}
+            onClick={() => addThreshold('breakdown')}
+          >
+            增加阈值
+          </Button>
+        )}
+        <Row gutter={16}>
+          {jsonNumThreshold.breakdown?.map((item, index) =>
+            renderThresholdItem(item, index, 'breakdown'),
+          )}
+        </Row>
+      </Form.Item>
+    </Modal>
+  );
+}

+ 30 - 0
src/components/ThresholdDetail/index.js

@@ -0,0 +1,30 @@
+import NewNumberBar from './NewNumberBar';
+
+function ThresholdDetail(props) {
+  const { data = {}, edit, onChange, onClick, current } = props;
+  const { Type, JsonNumThreshold, ThresholdEnum } = data;
+
+  if (Type == 2) {
+    return (
+      <div onClick={onClick}>
+        {/* <NumberBar
+          scale
+          current={current}
+          breakdown={JsonNumThreshold?.breakdown}
+          exception={JsonNumThreshold?.exception}
+        /> */}
+        <NewNumberBar
+          scale
+          current={current}
+          breakdown={JsonNumThreshold?.breakdown}
+          exception={JsonNumThreshold?.exception}
+        />
+      </div>
+    );
+  } else if (Type == 0) {
+    return <div>文本</div>;
+  } else {
+    return <div>枚举({ThresholdEnum})</div>;
+  }
+}
+export default ThresholdDetail;

+ 96 - 0
src/components/ThresholdDetail/index.less

@@ -0,0 +1,96 @@
+.bar {
+  padding: 0 30%;
+  overflow: hidden;
+}
+
+.success {
+  background-color: #60FE76;
+  width: 300%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.box {
+  width: 100%;
+  height: 6px;
+  position: relative;
+  background-color: #60FE76;
+
+  .exception,
+  .breakdown {
+    height: 100%;
+    position: absolute;
+  }
+
+  .breakdown {
+    z-index: 2;
+    background-color: #D45C41;
+  }
+
+  .exception {
+    z-index: 1;
+    background-color: #FFE26D;
+  }
+
+  
+
+  .scaleBlackLeft,
+  .scaleBlackRight {
+    position: absolute;
+    width: 6px;
+    height: 100%;
+    background: #0D1A2B;
+    top: 0;
+    z-index: 20;
+  }
+
+  .scaleBlackLeft {
+    left: -3px;
+  }
+
+  .scaleBlackRight {
+    right: -3px;
+  }
+
+  
+}
+.scale {
+  position: absolute;
+  top: 18px;
+  line-height: 1.2;
+  font-size: 16px;
+  word-break: keep-all;
+  transform: translateX(-50%);
+
+  &.top {
+    top: inherit;
+    bottom: 18px;
+  }
+}
+.current {
+  position: absolute;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 10;
+  // height: 120%;
+  width: 9px;
+  height: 22px;
+  // background: url("@/assets/current.png") no-repeat center;
+  background-size: 100% 100%;
+  // .currentBar {
+  //   height: 100%;
+  //   width: 4px;
+  //   background-color: #fff;
+  // }
+}
+
+.sectionOut{
+  display: flex;
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  top: 0; 
+}

+ 46 - 0
src/pages/EqSelfInspection/ReportDetail.js

@@ -0,0 +1,46 @@
+import { UnityAction } from '@/utils/utils';
+import { connect, history, useParams } from '@umijs/max';
+import { Button } from 'antd';
+import { useEffect } from 'react';
+import Detail from './components/Detail';
+import styles from './index.less';
+
+const ReportDetail = (props) => {
+  const { data, dispatch, loading } = props;
+
+  const { projectId, routeId } = useParams();
+  useEffect(() => {
+    if (routeId) {
+      dispatch({
+        type: 'eqSelfInspection/getPatrolDataById',
+        payload: {
+          routeId,
+        },
+      });
+    }
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.page}>
+        {routeId && (
+          <Button
+            style={{ marginBottom: 20 }}
+            type="primary"
+            onClick={() => {
+              UnityAction.sendMsg('detailToList', '');
+              history.go(-1);
+            }}
+          >
+            返回
+          </Button>
+        )}
+        <Detail data={data} projectId={projectId} loading={loading} />
+      </div>
+    </div>
+  );
+};
+export default connect(({ loading, eqSelfInspection }) => ({
+  data: eqSelfInspection.autoReport,
+  loading: loading.models.eqSelfInspection,
+}))(ReportDetail);

+ 409 - 0
src/pages/EqSelfInspection/Statistics.js

@@ -0,0 +1,409 @@
+import {
+  patrolOverview,
+  patrolOverviewLine,
+  patrolOverviewPie,
+} from '@/services/eqSelfInspection';
+import { history, useParams, useRequest } from '@umijs/max';
+import { Button, Spin } from 'antd';
+import dayjs from 'dayjs';
+import * as echarts from 'echarts';
+import { useEffect, useRef } from 'react';
+import styles from './index.less';
+
+const defaultTime = {
+  s_time: dayjs().subtract(7, 'days').format('YYYY-MM-DD'),
+  e_time: dayjs().format('YYYY-MM-DD'),
+};
+
+const Statistics = (props) => {
+  const { projectId } = useParams();
+  const lineDomRef = useRef(null);
+  const LineChartRef = useRef(null);
+
+  const pieDomRef = useRef(null);
+  const pieChartRef = useRef(null);
+
+  const renderChart = () => {
+    if (!LineChartRef.current || !pieChartRef.current) return;
+    if (!lineData || !pieData) return;
+    LineChartRef.current.clear();
+    const lineOptions = getLineOption(
+      lineData?.time,
+      lineData?.data,
+      '系统自测异常统计',
+    );
+
+    // 设置图表配置项
+    LineChartRef.current.setOption(lineOptions);
+
+    pieChartRef.current.clear();
+    const pieOptions = getPieOption(pieData, '异常类型统计');
+
+    // 设置图表配置项
+    pieChartRef.current.setOption(pieOptions);
+    // 构建图表的配置项
+  };
+
+  const {
+    data: lineData,
+    run,
+    loading,
+  } = useRequest(patrolOverviewLine, {
+    defaultParams: [
+      {
+        projectId: Number(projectId),
+        ...defaultTime,
+      },
+    ],
+    formatResult(resData) {
+      let data = [];
+      let time = [];
+      resData?.data?.forEach((item) => {
+        data.push(item.val);
+        time.push(dayjs(item.key).format('MM-DD'));
+      });
+      return { data, time };
+    },
+  });
+
+  const {
+    data: pieData,
+    run: pieRun,
+    loading: pieLoading,
+  } = useRequest(patrolOverviewPie, {
+    defaultParams: [
+      {
+        projectId: Number(projectId),
+        ...defaultTime,
+      },
+    ],
+    formatResult(resData) {
+      return resData?.data?.map((item) => {
+        return { name: item.key, value: item.val };
+      });
+    },
+  });
+
+  const { data: overviewData, loading: overviewLoading } = useRequest(
+    patrolOverview,
+    {
+      defaultParams: [
+        {
+          projectId: Number(projectId),
+        },
+      ],
+      formatResult(resData) {
+        return [
+          { num: resData?.data?.today_check_num, label: '今日自检次数' },
+          { num: resData?.data?.today_exception_num, label: '今日异常次数' },
+          { num: resData?.data?.all_check_num, label: '累计自检次数' },
+          { num: resData?.data?.all_check_day, label: '累计自检天数' },
+        ];
+      },
+    },
+  );
+
+  const getChartData = ({ s_time, e_time }) => {
+    run({ projectId: Number(projectId), s_time, e_time });
+    pieRun({ projectId: Number(projectId), s_time, e_time });
+  };
+  const onRadioChange = (e) => {
+    console.log(e);
+    let params = { s_time: defaultTime.s_time, e_time: defaultTime.e_time };
+    if (e == 'month')
+      params = {
+        s_time: dayjs().subtract(1, 'month').format('YYYY-MM-DD'),
+        e_time: dayjs().format('YYYY-MM-DD'),
+      };
+    else if (e == 'year')
+      params = {
+        s_time: dayjs().subtract(1, 'year').format('YYYY-MM-DD'),
+        e_time: dayjs().format('YYYY-MM-DD'),
+      };
+    getChartData(params);
+  };
+
+  useEffect(() => {
+    renderChart();
+  }, [lineData, pieData]);
+
+  useEffect(() => {
+    LineChartRef.current = echarts.init(lineDomRef.current);
+    pieChartRef.current = echarts.init(pieDomRef.current);
+    return () => {
+      LineChartRef.current.dispose();
+      pieChartRef.current.dispose();
+    };
+  }, []);
+  return (
+    <div>
+      <Button
+        type="primary"
+        style={{ marginBottom: 12 }}
+        onClick={() => {
+          history.go(-1);
+        }}
+      >
+        返回
+      </Button>
+      <div className={styles.itemMain}>
+        <div style={{ display: 'flex' }}>
+          {overviewData?.map((item) => (
+            <Text num={item.num} label={item.label} />
+          ))}
+        </div>
+      </div>
+      <div className={styles.itemMain}>
+        <div className={styles.tabs}>
+          近一周数据统计
+          {/* {radioData?.map(item => (
+          <div
+            key={item.key}
+            className={`${styles.item} ${active == item.key ? styles.active : ''}`}
+            onClick={() => {
+              setActive(item.key);
+              onRadioChange(item.key);
+            }}
+          >
+            {item.label}
+          </div>
+        ))} */}
+        </div>
+        <Spin spinning={loading}>
+          <div
+            ref={lineDomRef}
+            style={{ height: '340px', margin: '10px 0 10px 0' }}
+          />
+        </Spin>
+      </div>
+      <div className={styles.itemMain}>
+        <Spin spinning={pieLoading}>
+          <div
+            ref={pieDomRef}
+            style={{ height: '340px', margin: '10px 0 10px 0' }}
+          />
+        </Spin>
+      </div>
+    </div>
+  );
+};
+export default Statistics;
+
+const Text = (props) => {
+  const { num, label } = props;
+  return (
+    <div className={styles.statisticsText}>
+      <div className={styles.num}>{num}</div>
+      <div className={styles.label} style={{ fontSize: 22 }}>
+        {label}
+      </div>
+    </div>
+  );
+};
+
+const getLineOption = (time, chartData, name) => {
+  const y1Max = getMax(chartData);
+  const dataType = 'line';
+  const option = {
+    color: [
+      '#5470c6',
+      '#91cc75',
+      '#fac858',
+      '#ee6666',
+      '#73c0de',
+      '#3ba272',
+      '#fc8452',
+      '#9a60b4',
+      '#ea7ccc',
+    ],
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'cross',
+        crossStyle: {
+          color: '#999',
+        },
+      },
+    },
+    // grid: {
+    //   bottom: 30,
+    //   left: 60,
+    //   right: 30,
+    // },
+    xAxis: {
+      type: 'category',
+      axisPointer: {
+        type: 'shadow',
+      },
+      axisLine: {
+        lineStyle: {
+          color: '#c9d2d2',
+        },
+      },
+      axisLabel: {
+        fontSize: 16,
+      },
+      data: time,
+    },
+    yAxis: [
+      {
+        type: 'value',
+        top: 20,
+        // max: y1Max,
+        // interval: y1Max / 5,
+        // splitNumber: 5,
+        // nameTextStyle: {
+        //   color: '#fff',
+        //   fontSize: 16,
+        //   padding: [0, 0, 20, 0],
+        // },
+        axisLabel: {
+          color: '#c9d2d2',
+        },
+        axisLabel: {
+          fontSize: 16,
+          color: '#c9d2d2',
+        },
+        axisLine: {
+          show: false,
+        },
+        splitLine: {
+          lineStyle: {
+            type: 'dashed',
+          },
+        },
+      },
+    ],
+    series: [
+      {
+        data: chartData,
+        type: dataType,
+        name: '次数',
+        yAxisIndex: 0,
+        smooth: true,
+      },
+    ],
+    legend: {
+      textStyle: {
+        color: '#c9d2d2',
+        fontSize: 18,
+      },
+      lineStyle: {},
+    },
+    title: {
+      text: name,
+      top: '92%',
+      left: '50%',
+      textAlign: 'center',
+      textStyle: {
+        color: '#c9d2d2',
+        fontWeight: 'normal',
+        fontSize: 18,
+      },
+    },
+    // textStyle: {
+    //   fontSize: 50
+    // }
+  };
+
+  // if (chartData2) {
+  //   const y2Max = getMax(chartData2);
+  //   option.yAxis.push({
+  //     type: 'value',
+  //     max: y2Max,
+  //     interval: y2Max / 5,
+  //     splitNumber: 5,
+  //     top: 20,
+  //     position: 'right',
+  //     nameTextStyle: {
+  //       color: '#fff',
+  //       fontSize: 16,
+  //       // align: 'left',
+  //       padding: [0, 0, 20, 0],
+  //     },
+  //     axisLabel: {
+  //       color: '#fff',
+  //     },
+  //     axisLine: {
+  //       show: false,
+  //     },
+  //     splitLine: {
+  //       lineStyle: {
+  //         type: 'dashed',
+  //       },
+  //     },
+  //   });
+  //   option.series.push({
+  //     data: chartData2,
+  //     type: 'line',
+  //     name: active,
+  //     yAxisIndex: 1,
+  //   });
+  // }
+
+  return option;
+};
+
+const getPieOption = (chartData, name) => {
+  const option = {
+    title: {
+      text: name,
+      top: '92%',
+      left: '50%',
+      textAlign: 'center',
+      textStyle: {
+        color: '#c9d2d2',
+        fontWeight: 'normal',
+        fontSize: 18,
+      },
+    },
+    color: [
+      '#5470c6',
+      '#91cc75',
+      '#fac858',
+      '#ee6666',
+      '#73c0de',
+      '#3ba272',
+      '#fc8452',
+      '#9a60b4',
+      '#ea7ccc',
+    ],
+    tooltip: {
+      trigger: 'item',
+    },
+    legend: {
+      orient: 'horizontal',
+      // left: 'left',
+      textStyle: {
+        color: '#c9d2d2',
+        fontSize: 18,
+      },
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: '50%',
+        data: chartData,
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  };
+  return option;
+};
+
+function getMax(arr) {
+  const max = Math.max(...arr);
+  if (max == 100) return 100;
+  const exponent = Math.floor(Math.log10(max));
+  const base = Math.pow(10, exponent);
+  const remainder = max % base;
+  const maxRoundUp = max - remainder + base;
+  const maxFixed = maxRoundUp.toFixed(1); // 将最大值的小数位数限制为 1
+  return Number(maxFixed); // 将字符串转换为数字并返回
+}

+ 932 - 0
src/pages/EqSelfInspection/components/Detail.js

@@ -0,0 +1,932 @@
+import ThresholdDetail from '@/components/ThresholdDetail';
+import ThresholdModal from '@/components/ThresholdDetail/ThresholdModal';
+import { changeRecordStatus, getDumuDetail } from '@/services/eqSelfInspection';
+import { UnityAction } from '@/utils/utils';
+import { connect, useRequest } from '@umijs/max';
+import {
+  Card,
+  Col,
+  DatePicker,
+  Form,
+  Input,
+  Modal,
+  Row,
+  Select,
+  Spin,
+  Table,
+  Tabs,
+  message,
+} from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useMemo, useState } from 'react';
+import ReactZmage from 'react-zmage';
+import styles from './PatrolReportDetail.less';
+
+function Detail(props) {
+  const { data, userList, projectId, dispatch, loading = false } = props;
+
+  const [select, setSelect] = useState();
+  const [dumuList, setDumuList] = useState([]);
+  const sendMessageToUnity = (select, data) => {
+    setSelect(select);
+    // console.log(data);
+    if (window.HightlightEquipment) {
+      window.HightlightEquipment(data);
+    }
+  };
+  const getTextColor = (status) => {
+    switch (status) {
+      case '警告':
+        return '#FFE26D';
+      case '异常':
+        return '#FF8600';
+      case '正常':
+        return '#60FE76';
+      default:
+        return '#60FE76';
+    }
+  };
+  const { run: detailRun } = useRequest(getDumuDetail, {
+    manual: true,
+    fetchKey: (id) => id,
+    onSuccess: (data) => {
+      var item = dumuList?.find((child) => child.id === data.id);
+      if (item) {
+        item.url = base64ToImageUrl(data.event_bg);
+      }
+      setDumuList([...dumuList]);
+    },
+  });
+  const result = useMemo(() => {
+    var resultArr = [];
+    var tempResult = data?.PatrolResult;
+    var tempArr = tempResult?.split(';');
+    if (tempArr?.length > 0) {
+      tempArr?.forEach((item, index) => {
+        var tempItem = item;
+        var itemSplit = tempItem.split(':');
+        if (itemSplit?.length > 1) {
+          var label = '';
+          var value = itemSplit[1];
+          if (index === 0) label = '设备自检';
+          else if (index === 1) label = '工艺自检';
+          else {
+            label = '安全隐患';
+            value = data?.secureStatus === 0 ? '正常' : '异常';
+          }
+          resultArr.push({
+            label,
+            value,
+            color: getTextColor(value),
+          });
+        }
+      });
+    }
+    return resultArr;
+  }, [data]);
+
+  useEffect(() => {
+    dispatch({
+      type: 'eqSelfInspection/fetchUserList',
+      payload: {
+        projectId,
+      },
+    });
+  }, []);
+
+  useEffect(() => {
+    setDumuList(data?.dumuList);
+    data?.dumuList?.map((item) => {
+      detailRun(item.id);
+    });
+  }, [data?.dumuList]);
+
+  // useEffect(() => {
+  //   dispatch({
+  //     type: 'eqSelfInspection/getPatrolRecordMandateInfo',
+  //     payload: {
+  //       extend_id: data.Id,
+  //       project_id: projectId,
+  //       mandate_type: 2
+  //     },
+  //   });
+  // }, [data]);
+
+  return (
+    <Spin spinning={loading}>
+      <div className={styles.card}>
+        <Card title="自检报告">
+          <div>
+            <Row>
+              <Col span={24} style={{ fontSize: 20 }}>
+                自检时间:{data?.CreatedTime}
+              </Col>
+            </Row>
+            <Row>
+              <Col span={8} style={{ fontSize: 20 }}>
+                自检路线:{data?.RouteInfo?.Name}
+              </Col>
+              <Col span={8} style={{ fontSize: 20 }}>
+                工艺段:{data?.RouteInfo?.GroupID}
+              </Col>
+            </Row>
+            <Row>
+              {result?.map((item) => {
+                return (
+                  <Col span={8} style={{ display: 'flex', fontSize: 20 }}>
+                    <div>{item?.label}:</div>
+                    <div style={{ color: item.color }}>{item?.value}</div>
+                  </Col>
+                );
+              })}
+            </Row>
+            {/* <Row>
+            <Col span={8} style={{ display: 'flex', fontSize: 20 }}>
+              <div>任务类型:</div>
+              <div style={{ color: '#7bfffb' }}>系统自检</div>
+            </Col>
+            <Col span={8} style={{ display: 'flex', fontSize: 20 }}>
+              <div>任务负责人:</div>
+              <div style={{ color: '#fff' }}>{userList.find(item => item.ID === mandateInfo?.ResponsiblePeople)?.CName || ''}</div>
+            </Col>
+            <Col span={8} style={{ display: 'flex', fontSize: 20 }}>
+              <div>任务状态:</div>
+              <div style={{ color: '#7bfffb' }}>{getStatusText(mandateInfo?.Status)}</div>
+            </Col>
+          </Row> */}
+          </div>
+        </Card>
+      </div>
+
+      {/* 设备自检报告 */}
+      <ReportCom
+        sendMessageToUnity={sendMessageToUnity}
+        select={select}
+        waringData={data?.extendWarningData}
+        allData={data?.extendWarningAllData}
+        key="extend"
+        type={'extend'}
+        userList={userList}
+        title={
+          <>
+            <div className={styles.text}>设备自检报告</div>
+          </>
+        }
+      ></ReportCom>
+
+      {/* 工艺自检报告"> */}
+      <div>
+        <div className={styles.tabBarExtraContent}>
+          <div className={styles.text} style={{ height: 52, width: '50%' }}>
+            <>
+              <div>工艺自检报告</div>
+            </>
+          </div>
+          <div className={styles.abnormal}>
+            <div className={styles.text} style={{ float: 'right' }}>
+              异常({data?.FaultAnalysis?.length || 0})
+            </div>
+          </div>
+        </div>
+        <AalysisTable
+          onClickItem={sendMessageToUnity}
+          select={select}
+          data={data}
+        />
+      </div>
+      {/* 安全隐患自检报告"> */}
+      <div>
+        <div className={styles.tabBarExtraContent}>
+          <div className={styles.text} style={{ height: 52, width: '50%' }}>
+            <>
+              <div>安全隐患自检报告</div>
+            </>
+          </div>
+        </div>
+        {/* 环境异常 */}
+        <ReportCom
+          sendMessageToUnity={sendMessageToUnity}
+          select={select}
+          waringData={data?.sensorWaringData}
+          allData={data?.sensor}
+          data={data?.sensorWaringData}
+          key="sensor"
+          type={'sensor'}
+          userList={userList}
+          title={<div style={{ color: '#7bfffb', fontSize: 22 }}>环境异常</div>}
+        ></ReportCom>
+
+        {/* 安防检测异常 */}
+        <ReportDumCom
+          data={dumuList}
+          title={
+            <div style={{ color: '#7bfffb', fontSize: 22 }}>安防检测异常</div>
+          }
+        />
+
+        {/* 电器检测异常 */}
+        <ReportCom
+          sendMessageToUnity={sendMessageToUnity}
+          select={select}
+          waringData={[]}
+          allData={[]}
+          key="extend"
+          type={'extend'}
+          userList={userList}
+          title={
+            <div style={{ color: '#7bfffb', fontSize: 22 }}>电气检测异常</div>
+          }
+        ></ReportCom>
+
+        {/* 密闭空间检测异常 */}
+        <ReportCom
+          sendMessageToUnity={sendMessageToUnity}
+          select={select}
+          waringData={[]}
+          allData={[]}
+          key="extend"
+          type={'extend'}
+          userList={userList}
+          title={
+            <div style={{ color: '#7bfffb', fontSize: 22 }}>
+              密闭空间检测异常
+            </div>
+          }
+        ></ReportCom>
+      </div>
+
+      {/* </Card> */}
+    </Spin>
+  );
+}
+
+export function DeviceTable(props) {
+  const {
+    onClickItem,
+    data = {},
+    items,
+    onErrorHandle,
+    select,
+    userList,
+    type,
+  } = props;
+  const { ProjectId, Id } = data;
+  const [loading, setLoading] = useState(false);
+  const [visible, setVisible] = useState(false);
+  const [errVisible, setErrVisible] = useState(false);
+  const [currentItem, setCurrentItem] = useState({});
+  const isSensor = type == 'sensor';
+
+  const onClickThreshold = (record) => {
+    setCurrentItem(record);
+    setVisible(true);
+  };
+  const onClickError = (record) => {
+    setCurrentItem(record);
+    setErrVisible(true);
+  };
+  const handleError = async (values) => {
+    setLoading(true);
+    var res = await changeRecordStatus({
+      ...values,
+      Id: currentItem.Id,
+      DeviceCode: currentItem.DeviceCode,
+      DeviceName: currentItem.DeviceName,
+      RecordId: data.Id,
+      RepairMan: values.RepairMan * 1,
+    });
+    setLoading(false);
+    if (res) {
+      message.success('操作成功');
+      setErrVisible(false);
+    }
+  };
+  const columns = [
+    {
+      title: '设备名称',
+      width: '25%',
+      dataIndex: 'DeviceName',
+    },
+    {
+      title: '巡检项',
+      width: '20%',
+      dataIndex: 'TemplateItem.Name',
+    },
+    // {
+    //   title: '设备位号',
+    //   width: '16%',
+    //   dataIndex: 'DeviceCode',
+    // },
+    {
+      title: '设定值范围',
+      width: '30%',
+      render: (record) => (
+        <ThresholdDetail
+          current={record.Value || 0}
+          data={record || {}}
+          // onClick={() => onClickThreshold(record)}
+        />
+      ),
+    },
+    {
+      title: '状态',
+      width: '13%',
+      dataIndex: 'Status',
+      render: (Status) => {
+        switch (Status) {
+          case -1:
+          case 0:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#60FE76' }}
+                ></i>
+                正常
+              </div>
+            );
+          case 1:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FF8600' }}
+                ></i>
+                异常
+              </div>
+            );
+          case 2:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                警告
+              </div>
+            );
+        }
+      },
+    },
+  ];
+  const handleClickItem = (data) => {
+    if (!isSensor) {
+      onClickItem(`DeviceTable-${data.Id}`, {
+        type: data.Status,
+        deviceCode: data.DeviceCode,
+      });
+    } else {
+      onClickItem(`DeviceTable-${data.Id}`, {
+        // type: data.Status,
+        deviceCode: data.DeviceCode,
+        value: Number(data.Value || 0),
+        threshold: data.JsonNumThreshold,
+      });
+      UnityAction.sendMsg('SinglePowerEnvironFromWeb', JSON.stringify(data));
+    }
+  };
+
+  useEffect(() => {
+    console.log('温控', items);
+    if (isSensor)
+      UnityAction.sendMsg('PowerEnvironsFromWeb', JSON.stringify(items));
+  }, [items]);
+
+  if (!isSensor) {
+    columns.push({
+      title: '操作',
+      width: '12%',
+      render: (record) =>
+        record.Status == 1 && (
+          <a style={{ color: '#7BFFFB' }} onClick={() => onClickError(record)}>
+            异常处理
+          </a>
+        ),
+    });
+  }
+
+  return (
+    <div>
+      <Table
+        columns={columns}
+        dataSource={items}
+        rowKey="Id"
+        onRow={(data) => {
+          return {
+            onClick: () => {
+              handleClickItem(data);
+            },
+          };
+        }}
+        locale={{
+          emptyText: <Empty />,
+        }}
+        rowClassName={(record) =>
+          `DeviceTable-${record.Id}` == select ? styles.select : null
+        }
+      />
+      <ThresholdModal
+        open={visible}
+        data={currentItem.JsonNumThreshold}
+        onClose={() => setVisible(false)}
+      />
+      <ErrorHandleModal
+        open={errVisible}
+        userList={userList}
+        onCancel={() => setErrVisible(false)}
+        onOk={handleError}
+      />
+    </div>
+  );
+}
+
+function AalysisTable(props) {
+  const { onClickItem, data = {}, select } = props;
+  const { FaultAnalysis } = data;
+  const columns = [
+    {
+      title: '异常名称',
+      width: '18%',
+      dataIndex: 'device_name',
+    },
+    // {
+    //   title: '位号',
+    //   width: '15%',
+    //   dataIndex: 'device_code',
+    // },
+    {
+      title: '可能原因',
+      width: '30%',
+      render: (record) => record.reason,
+    },
+    {
+      title: '解决方案',
+      width: '52%',
+      render: (record) => {
+        if (record.fix_plan instanceof Array) {
+          return (
+            <div>
+              {record.fix_plan.map((item) => (
+                <div>
+                  {item.content}
+                  <br />
+                </div>
+              ))}
+            </div>
+          );
+        } else {
+          return record.fix_plan;
+        }
+      },
+    },
+  ];
+
+  return (
+    <div>
+      <Table
+        columns={columns}
+        dataSource={FaultAnalysis}
+        rowKey="device_code"
+        onRow={(data) => {
+          return {
+            onClick: () => {
+              onClickItem(`AalysisTable-${data.device_code}`, {
+                deviceCode: data.device_code,
+              });
+            }, // 点击行
+          };
+        }}
+        locale={{
+          emptyText: <Empty />,
+        }}
+        rowClassName={(record) =>
+          `AalysisTable-${record.device_code}` == select ? styles.select : null
+        }
+      />
+    </div>
+  );
+}
+
+function ErrorHandleModal(props) {
+  const { visible, onCancel, onOk, userList } = props;
+  const [form] = Form.useForm();
+  const status = form.getFieldValue('Status');
+  const handleOk = () => {
+    form.validateFields((error, values) => {
+      if (error) return;
+      onOk({
+        ...values,
+        PlanTime: values?.PlanTime?.format('YYYY-MM-DD HH:mm:ss'),
+      });
+    });
+  };
+
+  return (
+    <Modal
+      title="异常处理"
+      open={visible}
+      onCancel={onCancel}
+      onOk={handleOk}
+      destroyOnClose
+    >
+      <Form labelCol={{ span: 7 }} wrapperCol={{ span: 16 }}>
+        <Form.Item label="异常处理备注" name="ExceptionHandling">
+          <Input.TextArea />
+        </Form.Item>
+        <Form.Item
+          label="审核状态"
+          name="Status"
+          rules={[{ required: true, message: '请选择验收状态' }]}
+        >
+          <Select style={{ width: '100%' }} placeholder="请选择验收状态">
+            <Select.Option value={1}>已派遣</Select.Option>
+            <Select.Option value={2}>已通过</Select.Option>
+          </Select>
+        </Form.Item>
+        <Form.Item
+          label="维修人"
+          name="RepairMan"
+          rules={[{ required: true, message: '请选择维修人' }]}
+        >
+          <Select
+            showSearch
+            placeholder="请选择维修人"
+            filterOption={(input, option) =>
+              option.props.children.indexOf(input) >= 0
+            }
+            style={{ width: '100%' }}
+          >
+            {userList?.map((item) => (
+              <Select.Option key={item.ID}>{item.CName}</Select.Option>
+            ))}
+          </Select>
+        </Form.Item>
+        {status == 1 && (
+          <>
+            <Form.Item
+              label="难度级别"
+              name="DifficultyLevel"
+              rules={[{ required: true, message: '请选择难度级别' }]}
+            >
+              <Select placeholder="请选择难度级别" style={{ width: '100%' }}>
+                <Select.Option value={0}>大修</Select.Option>
+                <Select.Option value={1}>项目维修</Select.Option>
+                <Select.Option value={2}>小修</Select.Option>
+              </Select>
+            </Form.Item>
+            <Form.Item
+              label="维修方式"
+              name="RepairType"
+              rules={[{ required: true, message: '请选择维修方式' }]}
+            >
+              <Select placeholder="请选择维修方式" style={{ width: '100%' }}>
+                <Select.Option value={0}>自维</Select.Option>
+                <Select.Option value={1}>外委</Select.Option>
+              </Select>
+            </Form.Item>
+            <Form.Item
+              label="计划完成日期"
+              name="PlanTime"
+              rules={[{ required: true, message: '请选择计划完成日期' }]}
+            >
+              <DatePicker />
+            </Form.Item>
+          </>
+        )}
+      </Form>
+    </Modal>
+  );
+}
+
+export function WarningTable(props) {
+  const {
+    onClickItem,
+    data = {},
+    onErrorHandle,
+    select,
+    userList,
+    type,
+    items,
+  } = props;
+  const { ProjectId, Id } = data;
+  const [loading, setLoading] = useState(false);
+  const [visible, setVisible] = useState(false);
+  const [errVisible, setErrVisible] = useState(false);
+  const [currentItem, setCurrentItem] = useState({});
+  const isSensor = type == 'sensor';
+
+  const onClickThreshold = (record) => {
+    setCurrentItem(record);
+    setVisible(true);
+  };
+  const onClickError = (record) => {
+    setCurrentItem(record);
+    setErrVisible(true);
+  };
+  const handleError = async (values) => {
+    setLoading(true);
+    var res = await changeRecordStatus({
+      ...values,
+      Id: currentItem.Id,
+      DeviceCode: currentItem.DeviceCode,
+      DeviceName: currentItem.DeviceName,
+      RecordId: data.Id,
+      RepairMan: values.RepairMan * 1,
+    });
+    setLoading(false);
+    if (res) {
+      message.success('操作成功');
+      setErrVisible(false);
+    }
+  };
+  const columns = [
+    {
+      title: '设备名称',
+      width: '25%',
+      dataIndex: 'DeviceName',
+    },
+    {
+      title: '巡检项',
+      width: '20%',
+      dataIndex: 'TemplateItem.Name',
+    },
+    // {
+    //   title: '设备位号',
+    //   width: '16%',
+    //   dataIndex: 'DeviceCode',
+    // },
+    {
+      title: '设定值范围',
+      width: '30%',
+      render: (record) => (
+        <ThresholdDetail
+          current={record.Value || 0}
+          data={record || {}}
+          // onClick={() => onClickThreshold(record)}
+        />
+      ),
+    },
+    {
+      title: '状态',
+      width: '13%',
+      dataIndex: 'Status',
+      render: (Status) => {
+        switch (Status) {
+          case -1:
+          case 0:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#60FE76' }}
+                ></i>
+                正常
+              </div>
+            );
+          case 1:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FF8600' }}
+                ></i>
+                异常
+              </div>
+            );
+          case 2:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                警告
+              </div>
+            );
+        }
+      },
+    },
+  ];
+  const handleClickItem = (data) => {
+    if (!isSensor) {
+      onClickItem(`DeviceTable-${data.Id}`, {
+        type: data.Status,
+        deviceCode: data.DeviceCode,
+      });
+    } else {
+      onClickItem(`DeviceTable-${data.Id}`, {
+        // type: data.Status,
+        deviceCode: data.DeviceCode,
+        value: Number(data.Value || 0),
+        threshold: data.JsonNumThreshold,
+      });
+      UnityAction.sendMsg('SinglePowerEnvironFromWeb', JSON.stringify(data));
+    }
+  };
+
+  if (!isSensor) {
+    columns.push({
+      title: '操作',
+      width: '12%',
+      render: (record) =>
+        record.Status == 1 && (
+          <a style={{ color: '#7BFFFB' }} onClick={() => onClickError(record)}>
+            异常处理
+          </a>
+        ),
+    });
+  }
+
+  useEffect(() => {
+    if (isSensor)
+      UnityAction.sendMsg('PowerEnvironsFromWeb', JSON.stringify(items));
+  }, [items]);
+
+  return (
+    <div>
+      {/* <div className="table-total">当前列表总数 {Items?.length || 0}</div> */}
+      <Table
+        columns={columns}
+        dataSource={items}
+        rowKey="Id"
+        onRow={(data) => {
+          return {
+            onClick: () => {
+              handleClickItem(data);
+            },
+          };
+        }}
+        locale={{
+          emptyText: <Empty />,
+        }}
+        rowClassName={(record) =>
+          `DeviceTable-${record.Id}` == select ? styles.select : null
+        }
+      />
+      <ThresholdModal
+        open={visible}
+        data={currentItem.JsonNumThreshold}
+        onClose={() => setVisible(false)}
+      />
+      <ErrorHandleModal
+        open={errVisible}
+        userList={userList}
+        onCancel={() => setErrVisible(false)}
+        onOk={handleError}
+      />
+    </div>
+  );
+}
+
+function ReportCom(props) {
+  const {
+    sendMessageToUnity,
+    select,
+    waringData = [],
+    allData = [],
+    userList,
+    type,
+    title,
+    data,
+  } = props;
+  const [activeKey, setActiveKey] = useState('1');
+  const handleTabsChange = (activeKey) => {
+    setActiveKey(activeKey);
+  };
+  return (
+    <div className={styles.detailCard}>
+      <Tabs
+        defaultActiveKey="1"
+        tabBarExtraContent={
+          <div className={styles.tabBarExtraContent}>{title} </div>
+        }
+        onChange={handleTabsChange}
+      >
+        <Tabs.TabPane tab={`异常/警告(${waringData.length || 0})`} key="1">
+          {activeKey == '1' && (
+            <WarningTable
+              onClickItem={sendMessageToUnity}
+              select={select}
+              items={waringData}
+              key={type}
+              data={data}
+              type={type}
+              userList={userList}
+            />
+          )}
+        </Tabs.TabPane>
+        <Tabs.TabPane tab={`全部(${allData.length || 0})`} key="2">
+          {activeKey == '2' && (
+            <DeviceTable
+              onClickItem={sendMessageToUnity}
+              select={select}
+              items={allData}
+              data={data}
+              key={type}
+              type={type}
+              userList={userList}
+            />
+          )}
+        </Tabs.TabPane>
+      </Tabs>
+    </div>
+  );
+}
+
+function ReportDumCom(props) {
+  const { data = [], title } = props;
+  const columns = [
+    {
+      title: '报警时间',
+      dataIndex: 'event_time',
+      render: (time) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'device_name',
+    },
+    {
+      title: '报警类型',
+      dataIndex: 'event_type',
+      // render: type => alarmType[type],
+    },
+    {
+      title: '报警图片',
+      render: (item) => (
+        <ReactZmage
+          controller={{
+            // 关闭按钮
+            close: true,
+            // 旋转按钮
+            rotate: true,
+            // 缩放按钮
+            zoom: false,
+            // 下载按钮
+            download: false,
+            // 翻页按钮
+            flip: false,
+            // 多页指示
+            pagination: false,
+          }}
+          backdrop="rgba(255,255,255,0.5)"
+          style={{ height: '90px' }}
+          src={item.url}
+        />
+      ),
+    },
+  ];
+  return (
+    <div>
+      <div className={styles.tabBarExtraContent}>
+        <div className={styles.text} style={{ height: 52, width: '60%' }}>
+          <>
+            <div>{title}</div>
+          </>
+        </div>
+        <div className={styles.abnormal}>
+          <div className={styles.text} style={{ float: 'right' }}>
+            异常({data?.length || 0})
+          </div>
+        </div>
+      </div>
+      <Table
+        bordered
+        rowKey="event_time"
+        columns={columns}
+        dataSource={data}
+        locale={{
+          emptyText: <Empty />,
+        }}
+      />
+    </div>
+  );
+}
+
+function base64ToImageUrl(base64String) {
+  const byteCharacters = atob(base64String);
+  const byteArrays = [];
+
+  for (let i = 0; i < byteCharacters.length; i++) {
+    byteArrays.push(byteCharacters.charCodeAt(i));
+  }
+
+  const byteArray = new Uint8Array(byteArrays);
+  const blob = new Blob([byteArray], { type: 'image/png' });
+  const imageUrl = URL.createObjectURL(blob);
+
+  return imageUrl;
+}
+
+function Empty() {
+  return (
+    <div style={{}}>
+      {/* <img src={require('@/assets/empty.png')} style={{ margin: '20px 0' }} /> */}
+      <p style={{ textAlign: 'center' }}>自检正常</p>
+    </div>
+  );
+}
+
+export default connect(({ eqSelfInspection }) => ({
+  userList: eqSelfInspection.userList,
+  mandateInfo: eqSelfInspection.mandateInfo,
+}))(Detail);

+ 75 - 0
src/pages/EqSelfInspection/components/PatrolReportDetail.less

@@ -0,0 +1,75 @@
+.page {
+  padding-bottom: 20px;
+
+  :global {
+    .ant-card-head-title {
+      margin-left: 0;
+    }
+  }
+}
+
+.select {
+  background: #3682b2;
+  border-radius: 8px;
+  border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.iconStatus {
+  width: 20px;
+  height: 20px;
+  vertical-align: middle;
+  margin-right: 10px;
+  border-radius: 50%;
+  display: inline-block;
+}
+.detailCard {
+  margin: 10px 0 10px 0;
+  :global {
+    .ant-tabs-bar {
+      display: flex;
+      flex-direction: row;
+    }
+    .ant-tabs-extra-content {
+      width: 50%;
+    }
+    .ant-tabs-nav-container {
+      width: 50%;
+    }
+    .ant-tabs-nav-wrap {
+      background: none;
+      font-size: 18px;
+      width: 300px;
+      float: right;
+    }
+  }
+}
+.text {
+  .tabBarExtraContent;
+  font-size: 26px;
+  text-align: center;
+  color: white;
+  margin: 0 0 16px 0;
+}
+.abnormal {
+  // margin: 10px 0 10px 0;
+  height: 51px;
+  width: 50%;
+  .text {
+    color: #7bfffb;
+    font-size: 18px;
+  }
+}
+.tabBarExtraContent {
+  display: flex;
+  // justify-content: center;
+  align-items: center;
+  height: 100%;
+}
+.card {
+  :global {
+    .ant-card-head-title {
+      display: flex;
+      justify-content: center;
+    }
+  }
+}

+ 317 - 0
src/pages/EqSelfInspection/index.js

@@ -0,0 +1,317 @@
+import { GetTokenFromUrl, UnityAction } from '@/utils/utils';
+import { FundFilled } from '@ant-design/icons';
+import { connect, history, useLocation, useParams } from '@umijs/max';
+import { Button, Form, Modal, Select, Spin, message } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const EqSelfInspection = (props) => {
+  const { dispatch, autoReport, patrolList, loading } = props;
+  const { projectId } = useParams();
+  const { routeId } = useLocation();
+  const [form] = Form.useForm();
+  const [visible, setVisible] = useState(false);
+  // 0 正常 1 异常 2 loading
+  const [patrolStatus, setPatrolStatus] = useState(0);
+  const [faultAnalysisStatus, setFaultAnalysisStatus] = useState(0);
+  const [secureStatus, setSecureStatus] = useState(0);
+  const [secureChildren, setSecureChildren] = useState([]);
+  const getDate = () => {
+    dispatch({
+      type: 'eqSelfInspection/getAutoPatrol',
+      payload: {
+        projectId,
+      },
+    });
+  };
+
+  const HandleInspection = () => {
+    if (patrolList?.length === 0) {
+      message.error('请配置巡检路线!');
+    } else {
+      UnityAction.sendMsg('StartInspection', '');
+      dispatch({
+        type: 'eqSelfInspection/getAutoPatrol',
+        payload: { projectId },
+        callback: (data) => {
+          setPatrolStatus(data?.patrolStatus);
+          setFaultAnalysisStatus(data?.faultAnalysisStatus);
+          setSecureStatus(data?.secureStatus);
+          setSecureChildren(data?.secureChild);
+          history.replace(`/self-inspection/${projectId}?routeId=${data.Id}`);
+        },
+      });
+    }
+  };
+
+  const handleOk = () => {
+    UnityAction.sendMsg('StartInspection', '');
+    form.validateFields((err, values) => {
+      if (err) return;
+      dispatch({
+        type: 'eqSelfInspection/inspectionRoute',
+        payload: { projectId, routeId: values.routeId },
+        callback: (data) => {
+          setPatrolStatus(data?.patrolStatus);
+          setFaultAnalysisStatus(data?.faultAnalysisStatus);
+          setSecureStatus(data?.secureStatus);
+          setSecureChildren(data?.secureChild);
+          history.replace(`/self-inspection/${projectId}?routeId=${data.Id}`);
+        },
+      });
+    });
+    handleCancel();
+    setPatrolStatus(2);
+    setSecureStatus(2);
+    setFaultAnalysisStatus(2);
+  };
+
+  const handleCancel = () => {
+    setVisible(false);
+    form.resetFields();
+  };
+
+  useEffect(() => {
+    if (routeId) {
+      dispatch({
+        type: 'eqSelfInspection/getPatrolDataById',
+        payload: {
+          routeId: routeId,
+        },
+      });
+    } else if (autoReport.Id) {
+      dispatch({
+        type: 'eqSelfInspection/getPatrolDataById',
+        payload: {
+          routeId: autoReport.Id,
+        },
+      });
+    } else {
+      getDate();
+    }
+
+    dispatch({
+      type: 'eqSelfInspection/getList',
+      payload: {
+        ProjectId: projectId * 1,
+      },
+    });
+  }, []);
+
+  useEffect(() => {
+    setPatrolStatus(autoReport?.patrolStatus);
+    setFaultAnalysisStatus(autoReport?.faultAnalysisStatus);
+    setSecureStatus(autoReport?.secureStatus);
+    setSecureChildren(autoReport?.secureChild);
+  }, [autoReport]);
+
+  return (
+    <div>
+      <Spin spinning={loading} tip="正在自检中...">
+        <div className={styles.itemMain} style={{ padding: 20 }}>
+          <div style={{ position: 'relative', display: 'flex' }}>
+            <div style={{ fontSize: 24 }}>
+              自检间隔:{autoReport?.RouteInfo?.PlanDur}h
+            </div>
+            <div className={styles.icon}>
+              <FundFilled
+                onClick={() => {
+                  history.push(
+                    `/self-inspection/statistics/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+                  );
+                }}
+              />
+              {/* <DiffFilled
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  history.push(
+                    `/elf-ins-statistics/patrol-route/${projectId}/1/reocrd?JWT-TOKEN=${GetTokenFromUrl()}&isNew=${1}`,
+                  );
+                }}
+              />
+              <FundFilled
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  history.push(
+                    `/self-inspection/statistics/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+                  );
+                }}
+              /> */}
+            </div>
+          </div>
+          <div className={styles.logoInfo}>
+            <div className={styles.logo} />
+            <div style={{ display: 'flex', fontSize: 20 }}>
+              <div>系统自检发现</div>
+              <div
+                style={{
+                  color: autoReport?.warningTotalNum === 0 ? '#333' : '#FE5850',
+                  margin: '0 6px 0 6px',
+                }}
+              >
+                {autoReport?.warningTotalNum === 0
+                  ? '暂无'
+                  : autoReport?.warningTotalNum || 0}
+              </div>
+              <div>项异常</div>
+            </div>
+            <div style={{ fontSize: 20 }}>
+              {autoReport?.CreatedTime
+                ? dayjs(autoReport?.CreatedTime).format('YYYY-MM-DD HH:mm')
+                : ''}
+            </div>
+            <div className={styles.insbtn}>
+              <Button type="primary" onClick={HandleInspection}>
+                一键自检
+              </Button>
+              <div
+                style={{
+                  position: 'absolute',
+                  display: 'flex',
+                  alignItems: 'center',
+                  right: 0,
+                  height: '100%',
+                }}
+              >
+                <div>系统自检中</div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <Item name="设备自检" status={patrolStatus}></Item>
+        <Item name="工艺自检" status={faultAnalysisStatus}></Item>
+        <Item
+          name="安全自检"
+          warningText={
+            secureStatus === 1
+              ? `共发现${
+                  secureChildren?.filter((item) => item.status === 1)?.length ||
+                  0
+                }项异常`
+              : ''
+          }
+          status={secureStatus}
+        >
+          {secureChildren?.map((item) => (
+            <WarningItem label={item.label} status={item.status} />
+          ))}
+        </Item>
+        <div className={styles.reportBtn}>
+          <Button
+            type="primary"
+            onClick={() => {
+              history.push(
+                `/self-inspection/detail/${projectId}/${
+                  autoReport?.Id
+                }?JWT-TOKEN=${GetTokenFromUrl()}`,
+              );
+            }}
+          >
+            查看自检报告
+          </Button>
+        </div>
+
+        <Modal
+          title="选择巡检路线"
+          destroyOnClose
+          open={visible}
+          onOk={handleOk}
+          width="40%"
+          maskClosable={false}
+          onCancel={handleCancel}
+        >
+          <Form form={form} labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
+            <Form.Item
+              label="巡检路线"
+              name="routeId"
+              rules={[
+                {
+                  required: true,
+                  message: '请选择巡检路线',
+                },
+              ]}
+            >
+              <Select placeholder="请选择巡检路线">
+                {patrolList?.map((item) => (
+                  <Option key={item.Id} value={item.Id}>
+                    {item?.Name}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Form>
+        </Modal>
+      </Spin>
+    </div>
+  );
+};
+
+export default connect(({ eqSelfInspection, loading }) => ({
+  autoReport: eqSelfInspection.autoReport,
+  patrolList: eqSelfInspection.patrolList,
+  loading: loading.models['eqSelfInspection'],
+}))(EqSelfInspection);
+
+const Item = (props) => {
+  const { name, children, warningText = '', status = 0 } = props;
+  const renderRight = (status) => {
+    switch (status) {
+      case 0:
+        return (
+          <div className={styles.right} style={{ color: '#52c41a' }}>
+            正常
+          </div>
+        );
+      case 1:
+        return (
+          <div className={styles.right} style={{ color: '#FE5850' }}>
+            异常
+          </div>
+        );
+      case 2:
+        return;
+      // return <SyncOutlined className={styles.right} spin />;
+      default:
+        return (
+          <div className={styles.right} style={{ color: '#52c41a' }}>
+            正常
+          </div>
+        );
+    }
+  };
+  return (
+    <div
+      className={styles.itemMain}
+      style={{
+        paddingBottom: children ? 20 : 0,
+      }}
+    >
+      <div className={styles.item}>
+        <span>{name}</span>
+        <span className={styles.warningText}>{warningText}</span>
+        {renderRight(status)}
+      </div>
+      {children}
+    </div>
+  );
+};
+
+const WarningItem = (props) => {
+  const { label, status } = props;
+  return (
+    <div className={styles.warningItem}>
+      <span>{label}</span>
+      <div
+        style={{
+          color: status === 1 ? '#FE5850' : '#52c41a',
+          fontSize: 20,
+          position: 'absolute',
+          right: 20,
+        }}
+      >
+        {status === 0 ? '正常' : '异常'}
+      </div>
+    </div>
+  );
+};

+ 146 - 0
src/pages/EqSelfInspection/index.less

@@ -0,0 +1,146 @@
+.icon {
+  position: absolute;
+  right: 0;
+  font-size: 24px;
+  color: #40a9ff;
+  i {
+    margin-left: 10px;
+  }
+}
+.logoInfo {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  .logo {
+    // background: url('@/assets/newUI/defense.png') no-repeat center;
+    background-size: 100% 100%;
+    width: 144px;
+    height: 159px;
+  }
+  > div {
+    margin: 0px 0 8px 0;
+  }
+}
+.insbtn {
+  display: flex;
+  position: relative;
+  width: 100%;
+  justify-content: center;
+  :global {
+    .ant-btn-primary {
+      font-size: 20px;
+      width: 158px;
+      height: 40px;
+      cursor: pointer;
+    }
+  }
+}
+.item {
+  // border: 2px;
+  // border-radius: 12px;
+  // background-color: rgba(240, 248, 255, 0.549);
+  min-height: 60px;
+  display: flex;
+  align-items: center;
+  position: relative;
+  span:first-child {
+    margin-left: 10px;
+    font-size: 22px;
+  }
+  .right {
+    position: absolute;
+    right: 0;
+    margin-right: 10px;
+    font-size: 24px;
+  }
+  .warningText {
+    font-size: 20px;
+    color: #fe5850;
+    margin-left: 40px;
+  }
+}
+.warningItem {
+  // border-bottom: 2px solid #c9d2d2;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  margin: 0 60px 0 60px;
+  span {
+    font-size: 20px;
+  }
+}
+.reportBtn {
+  display: flex;
+  justify-content: center;
+  :global {
+  }
+}
+.itemMain {
+  // background: url('@/assets/newUI/tabsBg.png') no-repeat center;
+  background-size: 100% 100%;
+  border: 1px;
+  border-radius: 12px;
+  margin-bottom: 20px;
+  padding: 0 10px;
+}
+.statisticsText {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  flex: 1;
+  font-size: 26px;
+  .num {
+    color: #fadb14;
+  }
+  .label {
+    color: #c9d2d2;
+  }
+}
+.dialogBtns {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 30px;
+  margin-left: 40px;
+  :global {
+    .ant-btn.ant-btn-danger {
+      color: #4a4a4a;
+      background: #d5d5d5;
+      border-color: transparent;
+    }
+  }
+}
+.dialog {
+  // background: url('@/assets/newUI/eqDialog.png') no-repeat center;
+  width: 762px;
+  height: 234px;
+  .close {
+    // background: url('@/assets/newUI/close.png') no-repeat center;
+    background-size: 100% 100%;
+  }
+}
+.tabs {
+  display: flex;
+  // flex-direction: row-reverse;
+  display: flex;
+  color: #c9d2d2;
+  font-size: 22px;
+  justify-content: center;
+  .item {
+    padding: 4px 20px;
+    border: 1px solid #02a7f0;
+    font-size: 20px;
+    color: #fff;
+    cursor: pointer;
+
+    &:nth-child(1) {
+      border-left: 1px solid #02a7f0;
+    }
+
+    &.active {
+      background-color: #02a7f0;
+    }
+  }
+}

+ 273 - 0
src/pages/EqSelfInspection/models/eqSelfInspection.js

@@ -0,0 +1,273 @@
+import {
+  analysisResultList,
+  getAutoPatrolByRouteId,
+  getPatrolDumuList,
+  getPatrolRecordMandateInfo,
+  getRecentAutoPatrolByRouteId,
+  getRouteList,
+  patrolRelationList,
+  queryAnalysisDict,
+  queryPatrolRecord,
+  queryUserList,
+} from '@/services/eqSelfInspection';
+
+export default {
+  namespace: 'eqSelfInspection',
+  state: {
+    autoReport: {},
+    patrolList: [],
+    mandateInfo: [],
+  },
+  effects: {
+    *getAutoPatrol({ payload, callback }, { call, put }) {
+      const response = yield call(getRecentAutoPatrolByRouteId, payload);
+      if (response) {
+        // 查询数据报表详情
+        yield put({
+          type: 'getPatrolDataById',
+          payload: { routeId: response.data[0].Id },
+          callback: callback,
+        });
+      }
+    },
+    *fetchUserList({ payload }, { call, put }) {
+      const response = yield call(queryUserList, payload);
+      if (response) {
+        yield put({
+          type: 'userListHandler',
+          payload: response.data,
+        });
+      }
+    },
+    *getPatrolDataById({ payload, callback }, { call, put, select }) {
+      const { data } = yield call(queryPatrolRecord, {
+        recordId: payload.routeId,
+      });
+      if (data) {
+        const cacheData = yield select(
+          (state) => state.eqSelfInspection.autoReport,
+        );
+        if (cacheData.Id === data.Id) {
+          callback?.(cacheData);
+          return;
+        }
+        const creatorName = data.CreatorUser && data.CreatorUser.CName;
+        var status = {};
+        let Items = [],
+          sensor = [];
+        const getItems = (item, patrolType) => {
+          let key;
+          if (patrolType == 1) {
+            key = item.PatrolName;
+          } else {
+            key = item.DeviceCode + '-' + item.DeviceName;
+          }
+          item.TemplateItem = item.TemplateItem || {};
+          item.patrolType = patrolType;
+          item.ThresholdEnum = item.TemplateItem.ThresholdEnum;
+          item.Type = item.TemplateItem.Type;
+          if (!status[key]) status[key] = { normal: 0, error: 0 };
+          if (item.Status == 1) {
+            status[key].error++;
+          } else {
+            status[key].normal++;
+          }
+          return item;
+        };
+        let patrolCard = {};
+        let arr = [];
+        (data.ItemsExtend || []).forEach((item) => {
+          console.log(Items);
+          if (item.PatrolCardRecordItemAssocThreshold) {
+            var arr = item.PatrolCardRecordItemAssocThreshold.map((i) =>
+              getItems(i, 1),
+            );
+            Items = Items.concat(arr);
+
+            patrolCard[item.DeviceCode] =
+              item.PatrolCardRecordItemAssocThreshold;
+          } else {
+            Items.push(getItems(item, 0));
+
+            if (!patrolCard[item.DeviceCode]) patrolCard[item.DeviceCode] = [];
+            patrolCard[item.DeviceCode].push(item);
+          }
+        });
+        (data.ItemsSensor || []).forEach((item) => {
+          if (item.PatrolCardRecordItemAssocThreshold) {
+            var arr = item.PatrolCardRecordItemAssocThreshold.map((i) =>
+              getItems(i, 1),
+            );
+            sensor = sensor.concat(arr);
+          } else {
+            sensor.push(getItems(item, 0));
+          }
+        });
+
+        data.Items = Items;
+        data.sensor = sensor;
+
+        (data.Points || []).forEach((item) => {
+          let key;
+          if (item.PatrolType == 1) {
+            key = item.DeviceName;
+          } else {
+            key = item.DeviceCode + '-' + item.DeviceName;
+          }
+          item.creatorName = creatorName;
+          item.level = `${item.ExceptionLevel || '-'}/${
+            item.DeviceLevel || '-'
+          }`;
+          item.status = status[key] || { normal: 0, error: 0 };
+
+          if (patrolCard[item.DeviceCode] instanceof Array) {
+            arr = [...arr, ...patrolCard[item.DeviceCode]];
+          }
+        });
+        var ruleList = [46, 65, 92, 94];
+        if (ruleList.indexOf(data.ProjectId) != -1) {
+          const analysisDict = yield call(queryAnalysisDict, {
+            pageSize: 9999,
+          });
+          const analysisResult = yield call(analysisResultList, {
+            project_id: data.ProjectId,
+            id: data.Id,
+          });
+          const patrolRelation = yield call(patrolRelationList, {
+            ids: analysisResult?.data,
+            project_id: data.ProjectId,
+            page: 1,
+            page_size: 9999,
+          });
+          if (patrolRelation) {
+            let FaultAnalysis = [];
+            patrolRelation?.data?.list?.map((item) => {
+              var reason = analysisDict?.find(
+                (reason) => reason.id == item.Reason,
+              );
+              if (reason) {
+                item.Reason = reason.content;
+              }
+              var FixPlanArr = item.FixPlan.split(',');
+              if (FixPlanArr.length > 0) {
+                var FixPlan = [];
+                FixPlanArr.map((fixItem) => {
+                  var fixPlan = analysisDict?.find(
+                    (reason) => reason.id == fixItem,
+                  );
+                  if (fixPlan) FixPlan.push(fixPlan);
+                });
+                console.log(FixPlan);
+              }
+              FaultAnalysis.push({
+                device_name: item.DeviceName,
+                device_code: item.DeviceCode,
+                reason: item.Reason,
+                fix_plan: FixPlan,
+                c_time: item.CTime,
+              });
+            });
+            data.FaultAnalysis = FaultAnalysis;
+            console.log(data.FaultAnalysis);
+          }
+        }
+
+        const dumuList = yield call(getPatrolDumuList, {
+          record_id: payload.routeId,
+          project_id: data.ProjectId,
+        });
+        if (dumuList) {
+          data.dumuList = dumuList.data || [];
+        }
+        console.log(data);
+
+        data.sensorWaringData =
+          data.sensor?.filter(
+            (item) => item.Status === 2 || item.Status === 1,
+          ) || [];
+        data.sensorWaringNum = data.sensorWaringData.length;
+
+        data.extendWarningAllData = arr;
+        data.extendWarningData =
+          arr?.filter((item) => item.Status === 2 || item.Status === 1) || [];
+        data.extendWarningNum = data.extendWarningData?.length || 0;
+
+        let num = 0;
+        if (data?.extendWarningData?.length > 0) num += 1;
+        if (data?.sensorWaringNum > 0) num += 1;
+        if (data?.dumuList?.length > 0) num += 1;
+        if (data?.FaultAnalysis?.length > 0) num += 1;
+        data.warningTotalNum = num;
+
+        data.patrolStatus = data?.extendWarningData?.length > 0 ? 1 : 0;
+        data.faultAnalysisStatus = data?.FaultAnalysis?.length > 0 ? 1 : 0;
+        data.secureStatus =
+          data?.dumuList?.length > 0 || data?.sensorWaringNum > 0 ? 1 : 0;
+        let secureChild = [];
+        let dumuStatus = 0;
+        if (data?.dumuList?.length > 0) {
+          dumuStatus = 1;
+        }
+        secureChild.push({ label: '安防检测', status: dumuStatus });
+
+        let sensorStatus = 0;
+        if (data?.sensorWaringNum > 0) {
+          sensorStatus = 1;
+        }
+        secureChild.push({ label: '环境检测', status: sensorStatus });
+        secureChild.push({ label: '电气检测', status: 0 });
+        secureChild.push({ label: '密闭空间检测', status: 0 });
+
+        data.secureChild = secureChild;
+
+        callback?.(data);
+        yield put({
+          type: 'save',
+          payload: { autoReport: data },
+        });
+      }
+    },
+
+    *getList({ payload }, { call, put }) {
+      const response = yield call(getRouteList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: {
+            patrolList: response.data?.filter((item) => item.IsAuto === 1),
+          },
+        });
+      }
+    },
+    *getPatrolRecordMandateInfo({ payload }, { call, put }) {
+      const response = yield call(getPatrolRecordMandateInfo, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: {
+            mandateInfo: response?.data,
+          },
+        });
+      }
+    },
+    *inspectionRoute({ payload, callback }, { call, put, select }) {
+      //先通过巡检路线的Id申请接口,拿到最新的巡检记录Id,去查询
+      const response = yield call(getAutoPatrolByRouteId, payload);
+      if (response) {
+        yield put({
+          type: 'getAutoPatrol',
+          payload: { projectId: payload.projectId, route_id: payload.routeId },
+          callback: callback,
+        });
+      }
+    },
+  },
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 229 - 0
src/pages/Smart/ConditionDetection.js

@@ -0,0 +1,229 @@
+// 优化任务
+import {
+  queryProcessSection,
+  queryRealEstimate,
+  queryRealEstimateChart,
+} from '@/services/SmartOps';
+import { useParams, useRequest, history } from '@umijs/max';
+import { Button, Col, Row } from 'antd';
+import * as echarts from 'echarts';
+import { useEffect, useMemo, useRef } from 'react';
+
+import styles from './ConditionDetection.less';
+import CircleScore from './components/CircleScore';
+
+const ConditionDetection = (props) => {
+  const { projectId } = useParams();
+  let pid = Number(projectId);
+
+  // 查询工艺段列表
+  const { data: processList } = useRequest(queryProcessSection, {
+    defaultParams: [pid],
+  });
+
+  // 查询工况
+  const { data } = useRequest(queryRealEstimate, {
+    defaultParams: [pid],
+  });
+
+  const { score, real, best, grade } = useMemo(() => {
+    let score = '-',
+      grade = '-',
+      real = {},
+      best = {};
+
+    if (data) {
+      score = data.score;
+      if (score >= 90) {
+        grade = '优秀';
+      } else if (score >= 80) {
+        grade = '良好';
+      } else if (score >= 70) {
+        grade = '较好';
+      } else if (score >= 60) {
+        grade = '一般';
+      } else {
+        grade = '较差';
+      }
+      real = data.list[0] || {};
+      best = data.list[1] || {};
+    }
+
+    return { score, real, best, grade };
+  }, [data]);
+
+  return (
+    <div>
+      <Button
+        type="primary"
+        style={{ marginBottom: 20 }}
+        onClick={() => history.go(-1)}
+      >
+        返回
+      </Button>
+      <div className={styles.circle}>
+        <CircleScore>
+          <span className={styles.circleText}>{score}</span>
+        </CircleScore>
+        {/* <p>{desc}</p> */}
+        <p>膜车间当前运行状态{grade}</p>
+      </div>
+      <Row gutter={16}>
+        <Col span={12}>
+          <div className={styles.card}>
+            <h3>
+              实时工况 <span>{real.score}分</span>
+            </h3>
+            <ul>
+              <li>
+                <i></i>水质达标率评分:{real.water}
+              </li>
+              <li>
+                <i></i>能耗评分:{real.energy}
+              </li>
+              <li>
+                <i></i>药耗评分:{real.medicine}
+              </li>
+              <li>
+                <i></i>设施设备利用率评分:{real.device_rate}
+              </li>
+              <li>
+                <i></i>设施设备完好率评分:{real.device_intact}
+              </li>
+            </ul>
+          </div>
+        </Col>
+        <Col span={12}>
+          <div className={styles.card2}>
+            <h3>
+              目标工况 <span>{best.score}分</span>
+            </h3>
+            <ul>
+              <li>
+                <i></i>水质达标率评分:{best.water}
+              </li>
+              <li>
+                <i></i>能耗评分:{best.energy}
+              </li>
+              <li>
+                <i></i>药耗评分:{best.medicine}
+              </li>
+              <li>
+                <i></i>设施设备利用率评分:{best.device_rate}
+              </li>
+              <li>
+                <i></i>设施设备完好率评分:{best.device_intact}
+              </li>
+            </ul>
+          </div>
+        </Col>
+      </Row>
+      <ChartContent projectId={pid} />
+    </div>
+  );
+};
+
+const ChartContent = ({ projectId }) => {
+  const domRef = useRef(null);
+  const chartRef = useRef(null);
+
+  useRequest(queryRealEstimateChart, {
+    defaultParams: [projectId],
+    onSuccess(data) {
+      let options = getOption([
+        data.device_rate,
+        data.device_intact,
+        data.water,
+        data.energy,
+        data.medicine,
+      ]);
+      chartRef.current.setOption(options, true);
+    },
+  });
+
+  function getOption(data = []) {
+    const option = {
+      color: ['#FFC800', '#30EDFD', '#4096ff', '#ff4d4f', '#ffa940'],
+      tooltip: {
+        trigger: 'axis',
+      },
+      legend: {
+        textStyle: {
+          // color: '#fff',
+          fontSize: 18,
+        },
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: data[0].list?.map((item) => item.name),
+        axisLine: {
+          lineStyle: {
+            // color: '#fff',
+          },
+        },
+        splitLine: {
+          lineStyle: {
+            // color: '#fff',
+          },
+        },
+        axisLabel: {
+          // color: '#fff',
+        },
+      },
+      yAxis: {
+        type: 'value',
+        boundaryGap: true,
+        splitNumber: 5,
+        axisLine: {
+          lineStyle: {
+            // color: '#fff',
+          },
+        },
+        splitLine: {
+          lineStyle: {
+            // color: '#fff',
+          },
+        },
+        axisLabel: {
+          // color: '#fff',
+        },
+      },
+      series: data.map((item) => ({
+        name: item.name,
+        type: 'line',
+        showSymbol: false,
+        areaStyle: {
+          opacity: 0.1,
+        },
+        type: 'line',
+        smooth: true,
+        data: item?.list.map((v) => v.value),
+      })),
+    };
+
+    return option;
+  }
+
+  useEffect(() => {
+    chartRef.current = echarts.init(domRef.current);
+
+    return () => {
+      chartRef.current.dispose();
+    };
+  }, []);
+  return (
+    <div className={styles.card}>
+      <div className={styles.title}>近一日工况统计</div>
+      <div ref={domRef} style={{ height: '40vh' }}></div>
+    </div>
+  );
+};
+
+export default ConditionDetection;

+ 75 - 0
src/pages/Smart/ConditionDetection.less

@@ -0,0 +1,75 @@
+.card {
+  // background: url('@/assets/newUI/tabsBg.png') no-repeat center;
+  background-size: 100% 100%;
+  // color: #fff;
+  border-radius: 8px;
+  padding: 10px 20px;
+  margin: 10px 0;
+  h3 {
+    // color: #fff;
+    font-size: 24px;
+    margin-bottom: 10px;
+    letter-spacing: 2px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    span {
+      font-size: 24px;
+      color: #30EDFD;
+    }
+  }
+  ul {
+    margin: 0px;
+    padding: 0;
+    li {
+      margin-bottom: 10px;
+      font-size: 20px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+
+  i {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    background-color: #30EDFD;
+    border-radius: 50%;
+    margin-right: 8px;
+  }
+}
+
+.card2 {
+  .card;
+  i {
+    background-color: #FFC800;
+  }
+  h3 span {
+    color: #FFC800;
+  }
+}
+
+.circle {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  // color: #fff;
+  font-size: 24px;
+  p {
+    margin-top: 10px;
+    letter-spacing: 2px;
+  }
+}
+.circleText {
+  // color: #fff;
+  font-size: 44px;
+}
+
+.title {
+  font-size: 24px;
+  text-align: center;
+  // color: #fff;
+  margin-bottom: 10px;
+  letter-spacing: 2px;
+}

+ 220 - 0
src/pages/Smart/OptimizationTasks.js

@@ -0,0 +1,220 @@
+// 优化任务
+import {
+  queryMandate,
+  queryMandateChildList,
+  querySimulationProfit,
+  queryUserList
+} from '@/services/SmartOps';
+import { history, useLocation, useParams, useRequest } from '@umijs/max';
+import { Button, Col, Row, Table } from 'antd';
+import dayjs from 'dayjs';
+import { useMemo } from 'react';
+import styles from './OptimizationTasks.less';
+import Panel from './components/Panel';
+
+const OptimizationTasks = (props) => {
+  const { projectId } = useParams();
+  const { score } = useLocation();
+
+  const { data, run } = useRequest(queryMandate, {
+    manual: true,
+  });
+  const { data: userList } = useRequest(queryUserList, {
+    defaultParams: [{ projectId }],
+  });
+
+  const ResponsiblePeople = useMemo(() => {
+    if (!data || !userList) return '-';
+    let user = userList.data.find((item) => item.ID == data.ResponsiblePeople);
+    return user?.CName || '-';
+  }, [data, userList]);
+
+  const status = { 0: '未处理', 2: '已完成', 4: '已忽略', 5: '已派遣' };
+
+  return (
+    <div>
+      <Button
+        type="primary"
+        style={{ marginBottom: 20 }}
+        onClick={() => history.go(-1)}
+      >
+        返回
+      </Button>
+      <Produce
+        projectId={projectId}
+        score={score}
+        queryMandate={run}
+        mandate={data}
+      />
+      <Cost projectId={projectId} />
+
+      {data && (
+        <div
+          style={{ backgroundColor: '#a0bcda54', padding: 10, height: '100%' }}
+        >
+          <Row
+            gutter={16}
+            className={styles.detail}
+            style={{ marginBottom: 0 }}
+          >
+            <Col span={8}>
+              <div style={{ textAlign: 'center' }}>任务类型:系统发送</div>
+            </Col>
+            <Col span={8}>
+              <div style={{ textAlign: 'center' }}>
+                任务负责人:{ResponsiblePeople}
+              </div>
+            </Col>
+            <Col span={8}>
+              <div style={{ textAlign: 'center' }}>
+                任务状态:{status[data?.Status]}
+              </div>
+            </Col>
+          </Row>
+        </div>
+      )}
+    </div>
+  );
+};
+
+const Produce = ({ projectId, queryMandate }) => {
+  const columns = [
+    {
+      title: '参数',
+      dataIndex: 'Title',
+    },
+    {
+      title: '调整内容',
+      dataIndex: 'Content',
+    },
+  ];
+
+  const { data } = useRequest(queryMandateChildList, {
+    defaultParams: [
+      {
+        projectId,
+        mandateClass: 1,
+      },
+    ],
+    onSuccess(data) {
+      if (data.length > 0) {
+        queryMandate({
+          mandate_id: data[0]?.MandateId,
+        });
+      }
+    },
+  });
+
+  return (
+    <Panel
+      title="生产调度类"
+      style={{ marginBottom: 20, overflow: 'hidden', position: 'relative' }}
+    >
+      {data?.length > 0 && (
+        <>
+          <div
+            style={{
+              background: '#12CEB3',
+              width: 200,
+              position: 'absolute',
+              top: 30,
+              right: -55,
+              textAlign: 'center',
+              // color: '#fff',
+              fontSize: 18,
+              transform: 'rotate(45deg)',
+              padding: '4px 0',
+            }}
+          >
+            任务已发送
+          </div>
+          <h3 className={styles.title}>任务总结</h3>
+          <div className={styles.desc}>
+            根据水质相关数艍.建议您调节以下参数,水厂运行可达较优状态
+          </div>
+          <Table columns={columns} dataSource={data} />
+        </>
+      )}
+      {!data?.length && (
+        <>
+          <h3 className={styles.title}>任务总结</h3>
+          <div className={styles.desc}>
+            当前进水数据稳定,产水数据稳定,暂无需调节任务,继续保持哦~
+          </div>
+        </>
+      )}
+    </Panel>
+  );
+};
+
+const Cost = ({ projectId }) => {
+  const columns = [
+    {
+      title: '设备',
+      dataIndex: 'Title',
+    },
+    {
+      title: '调整内容',
+      dataIndex: 'Content',
+    },
+  ];
+
+  const { data: profit } = useRequest(querySimulationProfit, {
+    defaultParams: [
+      {
+        project_id: projectId,
+        s_time: dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
+        e_time: dayjs().format('YYYY-MM-DD'),
+      },
+    ],
+    formatResult(data) {
+      if (!data?.info) return '-';
+
+      return Object.values(data.info).reduce(
+        (total, currentValue) => total + currentValue,
+        0,
+      );
+    },
+  });
+  const { data } = useRequest(queryMandateChildList, {
+    defaultParams: [
+      {
+        projectId,
+        mandateClass: 2,
+      },
+    ],
+  });
+  return (
+    <Panel
+      title="成本节约类"
+      style={{ marginBottom: 20 }}
+      btns={
+        <Button
+          type="primary"
+          onClick={() => history.push(`/smart/simulate/${projectId}`)}
+        >
+          模拟评估
+        </Button>
+      }
+    >
+      {data?.length > 0 && (
+        <>
+          <h3 className={styles.title}>任务总结</h3>
+          <div className={styles.desc}>
+            通过能耗/药耗数据模拟仿真预计未来一日可节省
+            <span>{profit || '-'}</span>元
+          </div>
+          <Table columns={columns} dataSource={data} />
+        </>
+      )}
+      {!data?.length && (
+        <>
+          <h3 className={styles.title}>任务总结</h3>
+          <div className={styles.desc}>暂无可降低成本,继续保持哦~</div>
+        </>
+      )}
+    </Panel>
+  );
+};
+
+export default OptimizationTasks;

+ 15 - 0
src/pages/Smart/OptimizationTasks.less

@@ -0,0 +1,15 @@
+.title {
+  color: #eee;
+  font-size: 24px;
+  margin-bottom: 10px;
+}
+.desc {
+  color: #eee;
+  font-size: 22px;
+  margin-bottom: 20px;
+}
+.detail {
+  color: #eee;
+  font-size: 20px;
+  margin-bottom: 10px;
+}

+ 167 - 0
src/pages/Smart/Params/Edit.js

@@ -0,0 +1,167 @@
+import { addBaseParams, addOptimum, editOptimum } from '@/services/SmartOps';
+import {useRequest } from '@umijs/max';
+import { message } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import BasicParamsForm from './components/BasicParamsForm';
+import ControlParamsForm from './components/ControlParamsForm';
+import TargetOperatingConditions from './components/TargetOperatingConditions';
+import WorkConditionAssessment from './components/WorkConditionAssessment';
+import styles from './index.less';
+
+/**
+ *
+ * @param {number}
+ * @returns
+ */
+const Edit = (props) => {
+  const {
+    optimum,
+    process,
+    setProcess,
+    onSave,
+    setOptimum,
+    projectId,
+    refresh,
+    loading: listLoading,
+  } = props;
+  const sectionId = 7;
+  const [current, setCurrent] = useState();
+  // const [currentList, setCurrentList] = useState([]);
+  const [edit, setEdit] = useState(false);
+  const baseForm = useRef();
+  const controlForm = useRef();
+
+  const addBaseRes = useRequest(
+    (value) =>
+      addBaseParams({
+        base_value: JSON.stringify(value),
+        project_id: Number(projectId),
+        section_id: Number(sectionId),
+      }),
+    {
+      manual: true,
+      onSuccess(data) {
+        message.success('保存基础参数成功');
+        console.log(data);
+        setOptimum({ ...optimum, id: data.id });
+      },
+    },
+  );
+
+  const addRes = useRequest(addOptimum, {
+    manual: true,
+    onSuccess() {
+      setEdit(false);
+      refresh();
+      message.success('新增成功');
+    },
+  });
+
+  const updateRes = useRequest(editOptimum, {
+    manual: true,
+    onSuccess() {
+      setEdit(false);
+      refresh();
+      message.success('编辑成功');
+    },
+  });
+
+  // 新增工况
+  const onAddConditions = () => {
+    let newPorcess = JSON.parse(JSON.stringify(process));
+    newPorcess.base.forEach((item) => {
+      item.value = '';
+    });
+    setProcess(newPorcess);
+  };
+
+  const onEdit = (item) => {
+    setCurrent(item);
+    // setCurrentList(item.optimum_list)
+    setEdit(true);
+  };
+
+  const onSaveOptmium = (name) => {
+    let controlData = controlForm.current.getFieldsValue();
+
+    if (current) {
+      // 保存
+      updateRes.run({
+        name,
+        id: current.id,
+        control_value: JSON.stringify(controlData),
+      });
+    } else {
+      // 新增
+      addRes.run({
+        name,
+        bid: optimum.id,
+        project_id: Number(projectId),
+        section_id: Number(sectionId),
+        control_value: JSON.stringify(controlData),
+      });
+    }
+  };
+
+  useEffect(() => {
+    if (!current || Object.keys(current).length == 0) return;
+    process.control.forEach((item) => {
+      item.value = current.control?.[item.name];
+    });
+    process.base.forEach((item) => {
+      item.value = optimum.base?.[item.name];
+    });
+    setProcess({ ...process });
+  }, [current]);
+
+  const loading =
+    listLoading || addBaseRes.loading || updateRes.loading || addRes.loading;
+
+  return (
+    <>
+      <div className={styles.Row} style={{ marginBottom: 20 }}>
+        <div className={styles.Col}>
+          <WorkConditionAssessment process={process} projectId={projectId} />
+        </div>
+        <div className={styles.Col}>
+          <TargetOperatingConditions
+            list={optimum?.optimum_list || []}
+            current={current}
+            optimum={optimum}
+            onEdit={onEdit}
+            onCancel={onSave}
+            setCurrent={setCurrent}
+            loading={loading}
+            onDelete={(id) => updateRes.run({ id, is_delete: 1 })}
+          />
+        </div>
+      </div>
+      <div className={styles.Row}>
+        <div className={styles.Col}>
+          <BasicParamsForm
+            ref={baseForm}
+            process={process}
+            edit={true}
+            current={optimum}
+            loading={loading}
+            onSaveBase={addBaseRes.run}
+            changeProcess={(process) => setProcess(process)}
+          />
+        </div>
+        <div className={styles.Col}>
+          <ControlParamsForm
+            ref={controlForm}
+            process={process}
+            edit={true}
+            current={optimum}
+            onSave={onSaveOptmium}
+            loading={loading}
+            changeProcess={(process) => setProcess(process)}
+          />
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default Edit;

+ 312 - 0
src/pages/Smart/Params/Mock.js

@@ -0,0 +1,312 @@
+export default [
+  {
+    id: 1,
+    name: '机械加速澄清池',
+    base: [
+      {
+        name: '处理水量',
+        unit: '万m3/d',
+        max: 20,
+        // TODO: 根据接口或者随机生成
+        value: 19,
+      },
+      {
+        name: '进水CODMn',
+        unit: 'mg/L',
+        max: 20,
+        value: 18,
+      },
+      {
+        name: '进水浊度',
+        unit: 'NTU',
+        max: 20,
+        value: 19,
+      },
+    ],
+    control: [
+      {
+        name: 'FeCl3投加量',
+        unit: '吨/日',
+        max: 4,
+        value: 4,
+      },
+      {
+        name: '排泥量',
+        unit: 'm3/d',
+        max: 6000,
+        value: 5800,
+      },
+    ],
+    out: [
+      {
+        name: '产水CODMn',
+        unit: 'mg/L',
+        min: 5,
+        max: 10,
+        // TODO: 自动生成
+        realValue: 8,
+        mockValue: 8.1,
+      },
+      {
+        name: '产水浊度',
+        unit: 'NTU',
+        min: 0.6,
+        max: 1,
+        realValue: 0.7,
+        mockValue: 0.76,
+      },
+    ],
+  },
+  {
+    id: 2,
+    name: '主臭氧接触池',
+    base: [
+      {
+        name: '处理水量',
+        unit: '万m3/d',
+      },
+      {
+        name: '进水CODMn',
+        unit: 'mg/L',
+      },
+      {
+        name: '进水浊度',
+        unit: 'NTU',
+      },
+    ],
+    control: [
+      {
+        name: 'O3投加量',
+        unit: 'kg/d',
+      },
+      {
+        name: '水中O3浓度',
+        unit: 'mg/L',
+      },
+      {
+        name: '尾气O3浓度',
+        unit: 'mg/L',
+      },
+    ],
+    out: [
+      {
+        name: '产水CODMn',
+        unit: 'mg/L',
+        min: 5,
+        max: 10,
+      },
+      {
+        name: '产水浊度',
+        unit: 'NTU',
+        min: 0.6,
+        max: 1,
+      },
+    ],
+  },
+  {
+    id: 3,
+    name: '炭砂滤池',
+    base: [
+      {
+        name: '处理水量',
+        unit: '万m3/d',
+      },
+      {
+        name: '进水CODMn',
+        unit: 'mg/L',
+      },
+      {
+        name: '进水浊度',
+        unit: 'NTU',
+      },
+    ],
+    control: [
+      {
+        name: '过滤周期',
+        unit: 'h',
+      },
+      {
+        name: '反冲洗排水量',
+        unit: 'm3/d',
+      },
+    ],
+    out: [
+      {
+        name: '产水CODMn',
+        unit: 'mg/L',
+        min: 2,
+        max: 5,
+      },
+      {
+        name: '产水浊度',
+        unit: 'NTU',
+        min: 0.2,
+        max: 0.5,
+      },
+    ],
+  },
+  {
+    id: 4,
+    name: '泥线机加池',
+    base: [
+      {
+        name: '处理水量',
+        unit: '万m3./h',
+      },
+    ],
+    control: [
+      {
+        name: 'PAC投加量',
+        unit: 'kg/d',
+      },
+      {
+        name: 'FeCl3投加量',
+        unit: 'kg/日',
+      },
+    ],
+    out: [
+      {
+        name: '排泥浓度',
+        unit: 'g/L',
+        min: 20,
+        max: 26,
+      },
+      {
+        name: '上清液浊度',
+        unit: 'NTU',
+        min: 20,
+        max: 30,
+      },
+    ],
+  },
+  {
+    id: 5,
+    name: '排泥水处理车间',
+    base: [
+      {
+        name: '处理水量',
+        unit: '万m3./h',
+      },
+    ],
+    control: [],
+    out: [
+      {
+        name: '排泥浓度',
+        unit: 'g/L',
+        min: 15,
+        max: 20,
+      },
+      {
+        name: '排泥量',
+        unit: 'm3/h',
+        min: 10,
+        max: 15,
+      },
+    ],
+  },
+  {
+    id: 6,
+    name: '主加氯间',
+    base: [
+      {
+        name: '',
+        unit: '',
+      },
+    ],
+    control: [
+      {
+        name: '',
+        unit: '',
+      },
+    ],
+    out: [
+      {
+        name: '',
+        unit: '',
+        min: 5,
+        max: 10,
+      },
+    ],
+  },
+  {
+    id: 7,
+    name: '脱水机房',
+    base: [
+      {
+        name: '',
+        unit: '',
+      },
+    ],
+    control: [
+      {
+        name: '',
+        unit: '',
+      },
+    ],
+    out: [
+      {
+        name: '',
+        unit: '',
+        min: 5,
+        max: 10,
+      },
+    ],
+  },
+  {
+    id: 8,
+    name: '超滤膜',
+    base: [
+      {
+        name: '处理水量',
+        unit: '万m3/d',
+        max: 20,
+        value: 19,
+      },
+      {
+        name: '进水浊度',
+        unit: 'NTU',
+        max: 0.5,
+        value: 0.45,
+      },
+      {
+        name: '水温',
+        unit: '°C',
+        max: 20,
+        value: 18,
+      },
+    ],
+    control: [
+      {
+        name: '进水压力',
+        unit: 'Mpa',
+        max: 0.35,
+        value: 0.34,
+      },
+      {
+        name: 'TMP',
+        unit: 'Mpa',
+        max: 0.05,
+        value: 0.04,
+      },
+    ],
+    out: [
+      {
+        name: '产水量',
+        unit: '万m3/d',
+        min: 1.5,
+        max: 3,
+        // TODO: 自动生成
+        realValue: 1.69,
+        mockValue: 0,
+      },
+      {
+        name: '产水浊度',
+        unit: 'NTU',
+        min: 0.05,
+        max: 0.25,
+        realValue: 0.2,
+        mockValue: 0.2,
+      },
+    ],
+  },
+];

+ 77 - 0
src/pages/Smart/Params/View.js

@@ -0,0 +1,77 @@
+import { updateBase } from '@/services/SmartOps';
+import { useRequest } from '@umijs/max';
+import { message } from 'antd';
+import { useEffect, useState } from 'react';
+import BasicParamsForm from './components/BasicParamsForm';
+import ControlParamsForm from './components/ControlParamsForm';
+import TargetList from './components/TargetList';
+import WorkConditionAssessment from './components/WorkConditionAssessment';
+import styles from './index.less';
+
+/**
+ *
+ * @param {number}
+ * @returns
+ */
+const View = (props) => {
+  const {
+    listRes,
+    onEdit,
+    current,
+    setCurrent,
+    process,
+    setProcess,
+    refresh,
+    projectId,
+  } = props;
+  const sectionId = 7;
+  const [edit, setEdit] = useState(false);
+
+  const updateRes = useRequest(updateBase, {
+    manual: true,
+    onSuccess() {
+      refresh();
+      message.success('删除成功');
+    },
+  });
+  useEffect(() => {
+    if (!current || Object.keys(current).length == 0) return;
+    process.control.forEach((item) => {
+      item.value = current.control?.[item.name];
+    });
+    process.base.forEach((item) => {
+      item.value = current.base[item.name];
+    });
+    setProcess({ ...process });
+  }, [current]);
+
+  return (
+    <>
+      <div className={styles.Row} style={{ marginBottom: 20 }}>
+        <div className={styles.Col}>
+          <WorkConditionAssessment process={process} projectId={projectId} />
+        </div>
+        <div className={styles.Col}>
+          <TargetList
+            list={listRes.data}
+            loading={listRes.loading}
+            current={current}
+            onEdit={onEdit}
+            setCurrent={setCurrent}
+            onDelete={(id) => updateRes.run({ id, is_delete: 1 })}
+          />
+        </div>
+      </div>
+      <div className={styles.Row}>
+        <div className={styles.Col}>
+          <BasicParamsForm current={current} process={process} />
+        </div>
+        <div className={styles.Col}>
+          <ControlParamsForm current={current} process={process} />
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default View;

+ 96 - 0
src/pages/Smart/Params/components/BarChart.js

@@ -0,0 +1,96 @@
+import React, { useEffect, useRef, useState } from 'react';
+import * as echarts from 'echarts';
+
+
+const BarChart = props => {
+  const { data } = props;
+  const domRef = useRef(null);
+  const chartRef = useRef(null);
+
+  useEffect(() => {
+    chartRef.current = echarts.init(domRef.current);
+
+    // 在组件卸载时销毁图表实例
+    return () => {
+      chartRef.current.dispose();
+    };
+  }, []);
+
+  useEffect(() => {
+    let options = getOption(data);
+
+    // chartRef.current.clear();
+    chartRef.current.setOption(options);
+  }, [data]);
+
+  return <div ref={domRef} style={{ height: '100%' }} />;
+};
+const getOption = chartData => {
+  let option = {
+    title: [
+      {
+        text: chartData[0].name,
+        left: '25%',
+        bottom: 20,
+        textAlign: 'center',
+        textStyle: { // color: '#fff' },
+      },
+      {
+        text: chartData[1].name,
+        left: '75%',
+        bottom: 20,
+        textAlign: 'center',
+        textStyle: { // color: '#fff' },
+      },
+    ],
+    color: [
+      '#5470c6',
+      '#91cc75',
+      '#fac858',
+      '#ee6666',
+      '#73c0de',
+      '#3ba272',
+      '#fc8452',
+      '#9a60b4',
+      '#ea7ccc',
+    ],
+    tooltip: {
+      trigger: 'item',
+      formatter: '{a} <br/>{b} : {c}',
+    },
+    series: [
+      {
+        name: chartData[0].name,
+        type: 'pie',
+        radius: [20, "55%"],
+        center: ['25%', '45%'],
+        itemStyle: {
+          borderRadius: 5,
+        },
+        label: {
+          // color: '#fff',
+          fontSize: 16,
+        },
+        data: chartData[0].value,
+      },
+      {
+        name: chartData[1].name,
+        type: 'pie',
+        radius: [20, "55%"],
+        center: ['75%', '45%'],
+        itemStyle: {
+          borderRadius: 5,
+        },
+        label: {
+          // color: '#fff',
+          fontSize: 16,
+        },
+        data: chartData[1].value,
+      },
+    ],
+  };
+
+  return option;
+};
+
+export default BarChart;

+ 79 - 0
src/pages/Smart/Params/components/BasicParamsForm.js

@@ -0,0 +1,79 @@
+import { Button, Form, Input, Modal, Row } from 'antd';
+import Panel from '@/pages/Smart/components/Panel';
+
+const BasicParamsForm = ({
+  form,
+  process,
+  current,
+  edit,
+  loading,
+  changeProcess,
+  onSaveBase,
+}) => {
+  const base = current?.base || {};
+  const params = process.base;
+  const { getFieldDecorator } = form;
+
+  const canEdit = edit && !current?.id;
+
+  const renderFormItems = () => {
+    return params.map((param, index) => {
+      let key = param.name;
+      return (
+        <Row span={16} key={index}>
+          <Form.Item label={key}>
+            {getFieldDecorator(key, {
+              initialValue: base[key],
+              rules: [{ required: true }],
+            })(
+              <Input
+                disabled={!canEdit}
+                onChange={(e) => handleChangeProcess(key, e.target.value)}
+                addonAfter={param.unit}
+              />,
+            )}
+          </Form.Item>
+        </Row>
+      );
+    });
+  };
+
+  const handleChangeProcess = (key, value) => {
+    let data = form.getFieldsValue();
+    let params = process.base.find((item) => item.name == key);
+    params.value = value;
+
+    changeProcess({ ...process });
+  };
+  const onSave = () => {
+    Modal.confirm({
+      title: '提示',
+      content: '保存后无法再次修改!',
+      okText: '确定',
+      cancelText: '取消',
+      onOk: async () => {
+        let baseData = form.getFieldsValue();
+        onSaveBase(baseData);
+      },
+    });
+  };
+
+  return (
+    <Panel
+      title="基础参数"
+      btns={
+        canEdit && (
+          <Button loading={loading} onClick={onSave} type="primary">
+            保存
+          </Button>
+        )
+      }
+    >
+      <Form labelCol={{ span: 12 }} wrapperCol={{ span: 12 }}>
+        <div>{renderFormItems()}</div>
+      </Form>
+    </Panel>
+  );
+};
+
+export default Form.create()(BasicParamsForm);

+ 104 - 0
src/pages/Smart/Params/components/ControlParamsForm.js

@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import { Form, Input, Slider, Row, Col, Button, Modal, message } from 'antd';
+import Panel from '@/pages/Smart/components/Panel';
+
+const ControlParamsForm = ({ form, process, edit, onSave, loading, current, changeProcess }) => {
+  const [name, setName] = useState('');
+  const [visible, setVisible] = useState(false);
+  const control = current?.control || {};
+  const { getFieldDecorator, setFieldsValue } = form;
+
+  const params = process.control;
+
+  const renderFormItems = () => {
+    return params.map((param, index) => {
+      let key = param.name;
+      return (
+        <Row span={12} key={index}>
+          <Form.Item label={key}>
+            {getFieldDecorator(key, {
+              initialValue: control[key],
+            })(
+              <Input
+                disabled={!edit}
+                onChange={e => handleChangeProcess(key, e.target.value)}
+                addonAfter={param.unit}
+              />
+            )}
+
+            {edit &&
+              getFieldDecorator(key, {
+                initialValue: control[key],
+              })(
+                <Slider
+                  min={0}
+                  max={param.max}
+                  step={param.max < 1 ? 0.01 : 1}
+                  onChange={value => handleChangeProcess(key, value)}
+                />
+              )}
+          </Form.Item>
+        </Row>
+      );
+    });
+  };
+
+  const handleChangeProcess = (key, value) => {
+    let data = form.getFieldsValue();
+    let params = process.control.find(item => item.name == key);
+    params.value = value;
+
+    changeProcess({ ...process });
+  };
+
+  const onShowModal = () => {
+    if (!current?.id) {
+      message.error('请先保存基础参数');
+      return;
+    }
+    setVisible(true);
+  };
+
+  return (
+    <Panel
+      title="控制参数"
+      btns={
+        edit && (
+          <Button loading={loading} onClick={onShowModal} type="primary">
+            新增
+          </Button>
+        )
+      }
+    >
+      <Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
+        <div>{renderFormItems()}</div>
+      </Form>
+      <Modal
+        destroyOnClose
+        confirmLoading={loading}
+        visible={visible}
+        title={`创建目标工况`}
+        onCancel={() => {
+          setVisible(false);
+        }}
+        onOk={() => {
+          onSave(name);
+          setVisible(false);
+          setName('');
+        }}
+      >
+        <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
+          <Form.Item label="名称">
+            <Input
+              placeholder="请输入目标工况名称"
+              value={name}
+              onChange={e => setName(e.target.value)}
+            />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </Panel>
+  );
+};
+
+export default Form.create()(ControlParamsForm);

+ 137 - 0
src/pages/Smart/Params/components/RadarChartModule.js

@@ -0,0 +1,137 @@
+import { Empty } from 'antd';
+import * as echarts from 'echarts';
+import { useEffect, useRef } from 'react';
+
+//图表模块
+const RadarChartModule = (props) => {
+  const chartDomRef = useRef();
+  const chartRef = useRef();
+  const { indicator, data, name = '', color } = props;
+  useEffect(() => {
+    chartRef.current = echarts.init(chartDomRef.current);
+    window.addEventListener('resize', resetChart);
+    return () => window.removeEventListener('resize', resetChart);
+  }, []);
+
+  useEffect(() => {
+    if (!chartRef.current) return;
+    const option = { ...defaultOption };
+    if (color) option.color = color;
+    let chartData = data;
+    if (chartData.length > 1) {
+      option.legend = {
+        // type: 'scroll',
+        right: 20,
+        top: '20%',
+        orient: 'vertical',
+        textStyle: {
+          // color: '#fff',
+        },
+      };
+    } else {
+      chartData = data.map((item) => {
+        return {
+          ...item,
+          label: {
+            show: true,
+            position: 'insideBottom',
+            formatter: function (params) {
+              return params.value;
+            },
+          },
+        };
+      });
+    }
+    option.radar.indicator = indicator;
+    option.series = [{ ...option.series[0], data: chartData, name }];
+
+    // chartRef.current.clear();
+    chartRef.current.setOption(option);
+    // chartRef.current.resize();
+  }, [indicator, data]);
+
+  const resetChart = () => {
+    if (chartRef.current) chartRef.current.resize();
+  };
+
+  const getStyle = () => {
+    if (data && data.length != 0) return { width: '100%', height: '100%' };
+    else return { width: '100%', height: '100%', display: 'none' };
+  };
+
+  return (
+    <div style={{ height: '100%' }}>
+      <div style={getStyle()} ref={chartDomRef} />
+      {(!data || data.length == 0) && <Empty />}
+    </div>
+  );
+};
+export default RadarChartModule;
+const colors = [
+  '#fac858',
+  '#ee6666',
+  '#73c0de',
+  '#3ba272',
+  '#fc8452',
+  '#9a60b4',
+  '#ea7ccc',
+];
+const defaultOption = {
+  color: colors,
+  tooltip: {
+    trigger: 'item',
+  },
+  radar: {
+    name: {
+      textStyle: {
+        // color: '#fff',
+        fontSize: 18,
+      },
+    },
+    splitArea: {
+      areaStyle: {
+        color: [
+          'rgba(27, 133, 168, 0.8)',
+          'rgba(27, 133, 168, 0.7)',
+          'rgba(27, 133, 168, 0.6)',
+          'rgba(27, 133, 168, 0.4)',
+          'rgba(27, 133, 168, 0.3)',
+        ],
+        // shadowColor: 'rgba(0, 0, 0, 0.2)',
+        // shadowBlur: 10
+      },
+    },
+    axisLine: {
+      lineStyle: {
+        color: '#238BB6',
+      },
+    },
+    splitLine: {
+      show: false,
+    },
+    indicator: [
+      { name: 'Sales' },
+      { name: 'Administration' },
+      { name: 'Information Technology' },
+      { name: 'Customer Support' },
+      { name: 'Development' },
+      { name: 'Marketing' },
+    ],
+  },
+  series: [
+    {
+      name: '',
+      type: 'radar',
+      // tooltip: {
+      //   trigger: 'item',
+      // },
+      // symbol: 'none',
+      // areaStyle: {},
+      data: [
+        {
+          value: [2, 3, 0, 100, 0, 1],
+        },
+      ],
+    },
+  ],
+};

+ 103 - 0
src/pages/Smart/Params/components/TargetList.js

@@ -0,0 +1,103 @@
+import React, { useState, useMemo, useEffect } from 'react';
+import { List, Button, Icon, Row, Col, Modal, Input, Form, message } from 'antd';
+import Panel from '@/pages/Smart/components/Panel';
+import { history } from 'umi';
+
+const TargetList = ({ onEdit, current, setCurrent, onDelete, list: data, loading }) => {
+  const renderItem = (item, index) => {
+    const { name, base, id, is_best, control } = item;
+    const isCurrent = id == current?.id;
+
+    const renderParameters = () => {
+      let data = { ...base, ...control };
+      let items = Object.keys(data).map(name => {
+        return (
+          <Col span={8} key={name}>
+            <div style={{ fontSize: 18, // color: '#fff', margin: '5px 0' }}>
+              <span>{name}: </span>
+              <span>{data[name]}</span>
+            </div>
+          </Col>
+        );
+      });
+
+      const rows = [];
+      const colCount = 3;
+
+      for (let i = 0; i < items.length; i += colCount) {
+        const rowItems = items.slice(i, i + colCount);
+        const row = (
+          <Row gutter={16} key={i}>
+            {rowItems}
+          </Row>
+        );
+        rows.push(row);
+      }
+
+      return <div style={{ margin: '0 auto', width: '80%' }}>{rows}</div>;
+    };
+
+    const renderActions = () => {
+      let actions = [];
+      actions.push(
+        <Icon
+          onClick={() => {
+            onEdit(item);
+          }}
+          type="form"
+        />
+      );
+      actions.push(<Icon onClick={() => handleRemove(item.id)} type="delete" />);
+      return actions;
+    };
+
+    return (
+      <List.Item
+        className={{ active: isCurrent }}
+        style={{ cursor: 'pointer' }}
+        actions={renderActions()}
+        onClick={() => setCurrent(item)}
+      >
+        <List.Item.Meta title={name} description={renderParameters()} />
+      </List.Item>
+    );
+  };
+
+  // 删除模块
+  const handleRemove = id => {
+    Modal.confirm({
+      title: '提醒',
+      content: `确认删除工况吗?`,
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => {
+        onDelete(id);
+      },
+    });
+  };
+
+  return (
+    <Panel
+      title="目标工况"
+      btns={
+        <div>
+          <Button style={{ marginRight: 20 }} onClick={() => onEdit(null)} type="primary">
+            新增
+          </Button>
+          <Button onClick={() => history.go(-1)} type="primary">
+            返回
+          </Button>
+        </div>
+      }
+    >
+      <List
+        loading={loading}
+        dataSource={data}
+        renderItem={renderItem}
+        style={{ height: '626px', overflowY: 'auto' }}
+      />
+    </Panel>
+  );
+};
+
+export default TargetList;

+ 113 - 0
src/pages/Smart/Params/components/TargetOperatingConditions.js

@@ -0,0 +1,113 @@
+import React, { useState, useMemo, useEffect } from 'react';
+import { List, Button, Icon, Row, Col, Modal, Input, Form, message } from 'antd';
+import Panel from '@/pages/Smart/components/Panel';
+
+const TargetOperatingConditions = ({
+  optimum,
+  onCancel,
+  // current,
+  setCurrent,
+  onDelete,
+  list: data,
+  loading,
+}) => {
+  const renderItem = (item, index) => {
+    const { name, control, id, is_best } = item;
+    const base = optimum?.base;
+    // const isCurrent = id == current?.id;
+
+    const renderParameters = () => {
+      let data = { ...base, ...control };
+      let items = Object.keys(data).map(name => {
+        return (
+          <Col span={8} key={name}>
+            <div style={{ fontSize: 18, // color: '#fff', margin: '5px 0' }}>
+              <span>{name}: </span>
+              <span>{data[name]}</span>
+            </div>
+          </Col>
+        );
+      });
+
+      const rows = [];
+      const colCount = 3;
+
+      for (let i = 0; i < items.length; i += colCount) {
+        const rowItems = items.slice(i, i + colCount);
+        const row = (
+          <Row gutter={16} key={i}>
+            {rowItems}
+          </Row>
+        );
+        rows.push(row);
+      }
+
+      return <div style={{ margin: '0 auto', width: '80%' }}>{rows}</div>;
+    };
+
+    const renderTitle = () => {
+      if (item.is_best) {
+        return (
+          <>
+            {name}
+            <span style={{ fontSize: 18, color: '#52c41a', marginLeft: 10 }}>(最优)</span>
+          </>
+        );
+      }
+      return name;
+    };
+
+    const renderActions = () => {
+      let actions = [];
+      // actions.push(
+      //   <Icon
+      //     onClick={() => {
+      //       // setName(item.name);
+      //       onEdit(item);
+      //     }}
+      //     type="form"
+      //   />
+      // );
+      actions.push(<Icon onClick={() => handleRemove(item.id)} type="delete" />);
+
+      return actions;
+    };
+
+    return (
+      <List.Item
+        // className={{ active: isCurrent }}
+        style={{ cursor: 'pointer' }}
+        actions={renderActions()}
+        onClick={() => setCurrent(item)}
+      >
+        <List.Item.Meta title={renderTitle()} description={renderParameters()} />
+      </List.Item>
+    );
+  };
+
+  // 删除模块
+  const handleRemove = id => {
+    Modal.confirm({
+      title: '提醒',
+      content: `确认删除工况吗?`,
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => {
+        onDelete(id);
+      },
+    });
+  };
+
+  return (
+    <Panel title="工况列表" btns={<Button onClick={() => onCancel()}>返回</Button>}>
+      <List
+        loading={loading}
+        dataSource={data}
+        renderItem={renderItem}
+        style={{ height: '626px', overflowY: 'auto' }}
+      />
+    </Panel>
+  );
+};
+
+export default TargetOperatingConditions;

+ 103 - 0
src/pages/Smart/Params/components/WorkConditionAssessment.js

@@ -0,0 +1,103 @@
+import Panel from '@/pages/Smart/components/Panel';
+import { useMemo } from 'react';
+import BarChart from './BarChart';
+import RadarChartModule from './RadarChartModule';
+
+const WorkConditionAssessment = (props) => {
+  const { process, projectId } = props;
+
+  const radarData = useMemo(() => {
+    let data = { value: [] };
+    let indicator = [];
+    [...process.base, ...process.control].forEach((item) => {
+      data.value.push(item.value);
+      indicator.push({
+        name: item.name,
+        max: item.max,
+      });
+    });
+    return { indicator, data: [data] };
+  }, [process]);
+
+  const barData = useMemo(() => {
+    let chartData = [];
+
+    process.out.forEach((item, index) => {
+      let data = {
+        name: item.name,
+        value: [
+          {
+            // 实际
+            name: '实际',
+            value: item.realValue,
+          },
+          {
+            // 预测
+            name: '预测',
+            value: getRandomInRange(item.realValue),
+          },
+        ],
+      };
+
+      chartData.push(data);
+    });
+    return chartData;
+  }, [process]);
+
+  return (
+    <Panel title="工况评估">
+      <div style={{ position: 'relative', margin: '20px 0' }}>
+        <div
+          style={{
+            fontSize: 20,
+            padding: '0px 8px',
+            color: '#FFF',
+            background: '#fac858',
+            borderRadius: 4,
+            position: 'absolute',
+            top: 0,
+            left: 10,
+          }}
+        >
+          参数
+        </div>
+        <div style={{ height: 280 }}>
+          <RadarChartModule color={'#fac858'} name={'参数'} {...radarData} />
+        </div>
+      </div>
+      <div style={{ position: 'relative' }}>
+        <div
+          style={{
+            fontSize: 20,
+            padding: '0px 8px',
+            color: '#FFF',
+            background: '#ee6666',
+            borderRadius: 4,
+            position: 'absolute',
+            top: 0,
+            left: 10,
+          }}
+        >
+          输出
+        </div>
+        <div style={{ height: 320 }}>
+          <BarChart data={barData} />
+        </div>
+      </div>
+    </Panel>
+  );
+};
+function getRandomInRange(num) {
+  // 计算正负10%的范围
+  var range = num * 0.1;
+
+  // 生成随机值
+  var randomValue = Math.random() * (2 * range) - range;
+
+  // 计算结果并保留两位小数
+  var result = (num + randomValue).toFixed(2);
+
+  // 将结果转换为数值类型并返回
+  return Number(result);
+}
+export default WorkConditionAssessment;

+ 112 - 0
src/pages/Smart/Params/index.js

@@ -0,0 +1,112 @@
+import { queryBaseList } from '@/services/SmartOps';
+import { useParams, useRequest } from '@umijs/max';
+import { useEffect, useState } from 'react';
+import Edit from './Edit';
+import processList from './Mock';
+import View from './View';
+
+/**
+ *
+ * @param {number}
+ * @returns
+ */
+const Work = (props) => {
+  const { projectId } = useParams();
+  const sectionId = 7;
+  const [process, setProcess] = useState(processList[sectionId]);
+  const [optimum, setOptimum] = useState({});
+  // const [currentList, setCurrentList] = useState([]);
+  const [edit, setEdit] = useState(false);
+
+  const listRes = useRequest(queryBaseList, {
+    defaultParams: [
+      {
+        project_id: Number(projectId),
+        section_id: Number(sectionId),
+      },
+    ],
+    formatResult(data) {
+      let list = data.list || [];
+      list.forEach((item) => {
+        item.base = {};
+        item.optimumList = [];
+        try {
+          item.base = JSON.parse(item.base_value || '{}');
+          // 随机生成最优工况
+          let bestIndex = item.optimum_list
+            ? Math.floor(Math.random() * item.optimum_list.length)
+            : 0;
+          item.optimum_list?.forEach((optItem, index) => {
+            optItem.control = JSON.parse(optItem.control_value || '{}');
+
+            if (bestIndex == index) {
+              optItem.is_best = 1;
+              // 把best的数据复制给父级
+              item.control = optItem.control;
+              item.name = optItem.name;
+            }
+          });
+        } catch (e) {
+          console.error(e);
+        }
+      });
+      // if (list.length > 0) setOptimum(list[0]);
+      // setCurrentList(list);
+      if (optimum) {
+        let item = list.find((item) => item.id == optimum.id);
+        item && setOptimum(item);
+      }
+      return list;
+    },
+  });
+
+  const onEdit = (item) => {
+    setOptimum(item);
+    setEdit(true);
+  };
+
+  const onSave = () => {
+    setEdit(false);
+  };
+
+  useEffect(() => {
+    if (!optimum || Object.keys(optimum).length == 0) return;
+    process.control.forEach((item) => {
+      item.value = optimum.control?.[item.name] || '';
+    });
+    process.base.forEach((item) => {
+      item.value = optimum.base?.[item.name] || '';
+    });
+    setProcess({ ...process });
+  }, [optimum]);
+
+  return (
+    <div>
+      {edit ? (
+        <Edit
+          projectId={projectId}
+          optimum={optimum}
+          process={process}
+          onSave={onSave}
+          setOptimum={setOptimum}
+          setProcess={setProcess}
+          refresh={listRes.refresh}
+          loading={listRes.loading}
+        />
+      ) : (
+        <View
+          projectId={projectId}
+          listRes={listRes}
+          current={optimum}
+          process={process}
+          onEdit={onEdit}
+          setCurrent={setOptimum}
+          setProcess={setProcess}
+          refresh={listRes.refresh}
+        />
+      )}
+    </div>
+  );
+};
+
+export default Work;

+ 9 - 0
src/pages/Smart/Params/index.less

@@ -0,0 +1,9 @@
+.Row {
+  display: flex;
+  align-items: stretch;
+  justify-content: space-between;
+
+  .Col {
+    width: calc(50% - 10px);
+  }
+}

+ 20 - 0
src/pages/Smart/Simulate.js

@@ -0,0 +1,20 @@
+import { useParams } from '@umijs/max';
+import SimulateDetail from './components/SimulateDetail';
+import SimulatePie from './components/SimulatePie';
+import styles from './index.less';
+
+const Simulate = (props) => {
+  const { projectId } = useParams();
+  return (
+    <div>
+      <div className={styles.Row} style={{ marginBottom: 20 }}>
+        <SimulatePie projectId={projectId} />
+      </div>
+      <div className={styles.Row}>
+        <SimulateDetail projectId={projectId} />
+      </div>
+    </div>
+  );
+};
+
+export default Simulate;

+ 1 - 1
src/pages/Smart/components/CircleScore.less

@@ -20,6 +20,6 @@
   transform: translate(-50%, -50%);
   font-size: 34px;
   font-weight: bold;
-  color: #fff;
+  // color: #fff;
   text-align: center;
 }

+ 39 - 0
src/pages/Smart/components/Panel.js

@@ -0,0 +1,39 @@
+export default ({ title, children, btns, style, rightTitle }) => {
+  return (
+    <div
+      style={{
+        backgroundColor: '#a0bcda54',
+        padding: 10,
+        height: '100%',
+        ...style,
+      }}
+    >
+      <div
+        style={{
+          display: 'flex',
+          padding: '0 10px',
+          marginBottom: 10,
+          height: 40,
+          alignItems: 'center',
+          justifyContent: rightTitle ? 'right' : 'space-between',
+        }}
+      >
+        <div>
+          <div
+            style={{
+              float: rightTitle ? 'right' : 'left',
+              width: 8,
+              height: 30,
+              backgroundColor: '#366CDA',
+              marginLeft: rightTitle ? '10px' : '0',
+            }}
+          />
+          <span style={{ fontSize: 22, paddingLeft: 12 }}>{title}</span>
+        </div>
+        {btns}
+      </div>
+
+      {children}
+    </div>
+  );
+};

+ 783 - 0
src/pages/Smart/components/SimulateDetail.js

@@ -0,0 +1,783 @@
+import {
+  queryBackwash,
+  queryBackwashList,
+  queryDesignNob,
+  queryDesignNobList,
+  queryDesignWash,
+  queryDesignWashList,
+  queryDrug,
+  queryDrugList,
+  queryMembrane,
+  queryMembraneConditions,
+  queryMembraneList,
+  queryProjectConfig,
+} from '@/services/SmartOps';
+import { history, useParams, useRequest } from '@umijs/max';
+import {
+  Button,
+  Col,
+  DatePicker,
+  Empty,
+  Form,
+  Icon,
+  List,
+  Modal,
+  Row,
+  Spin,
+} from 'antd';
+import * as echarts from 'echarts';
+import dayjs from 'dayjs';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import Panel from './Panel';
+import styles from './SimulateDetail.less';
+
+const { RangePicker } = DatePicker;
+
+const TYPE = {
+  td_uf: {
+    name: '超滤膜',
+    device: (params) => queryMembraneList({ ...params, type: 'uf' }),
+    chart: (params) => queryMembrane({ ...params, type: 'uf' }),
+  },
+  td_mf: {
+    name: '微滤膜',
+    device: (params) => queryMembraneList({ ...params, type: 'mf' }),
+    chart: (params) => queryMembrane({ ...params, type: 'mf' }),
+  },
+  td_nf: {
+    name: '纳滤膜',
+    device: (params) => queryMembraneList({ ...params, type: 'nf' }),
+    chart: (params) => queryMembrane({ ...params, type: 'nf' }),
+  },
+  td_ro: {
+    name: '反渗透膜',
+    device: (params) => queryMembraneList({ ...params, type: 'ro' }),
+    chart: (params) => queryMembrane({ ...params, type: 'ro' }),
+  },
+  tdr_pac: {
+    name: '絮凝剂投加',
+    device: (params) => queryDrugList({ ...params, type: 'pac' }),
+    chart: (params) => queryDrug({ ...params, type: 'pac' }),
+  },
+  tdr_hci: {
+    name: 'HCI投加',
+    device: (params) => queryDrugList({ ...params, type: 'hci' }),
+    chart: (params) => queryDrug({ ...params, type: 'hci' }),
+  },
+  tdr_nob: {
+    name: '非氧化杀菌剂投加',
+    device: (params) => queryDrugList({ ...params, type: 'nob' }),
+    chart: (params) => queryDrug({ ...params, type: 'nob' }),
+  },
+  tt_backwash: {
+    name: '反冲洗记录',
+    device: queryBackwashList,
+    chart: queryBackwash,
+  },
+  tt_wash: {
+    name: '大水量冲洗记录',
+    device: queryDesignWashList,
+    chart: queryDesignWash,
+  },
+  tt_nob: {
+    name: '非氧化杀菌记录',
+    device: queryDesignNobList,
+    chart: queryDesignNob,
+  },
+};
+const SimulateDetail = (props) => {
+  const { projectId } = props;
+  const [active, setActive] = useState();
+  const [current, setCurrent] = useState();
+
+  const { data } = useRequest(queryProjectConfig, {
+    defaultParams: [projectId],
+    onSuccess(data) {
+      setActive(data[0]);
+    },
+  });
+
+  const onLeftClick = () => {
+    document.getElementById('content').scrollLeft -= 150;
+  };
+
+  const onRightClick = () => {
+    document.getElementById('content').scrollLeft += 150;
+  };
+
+  return (
+    <Panel
+      title={'模拟记录'}
+      style={{ width: '100%' }}
+      btns={
+        <div className={styles.tabs}>
+          <div className={styles.left} onClick={onLeftClick}></div>
+          <div id="content" className={styles.content}>
+            {data?.map((item) => (
+              <div
+                key={item}
+                className={`${styles.item} ${
+                  active == item ? styles.active : ''
+                }`}
+                onClick={() => {
+                  setActive(item);
+                  setCurrent(null);
+                }}
+              >
+                {TYPE[item]?.name}
+              </div>
+            ))}
+          </div>
+          <div className={styles.right} onClick={onRightClick}></div>
+        </div>
+      }
+    >
+      <div className={styles.box}>
+        <ListContent
+          active={active}
+          projectId={projectId}
+          current={current}
+          onClick={setCurrent}
+        />
+        <ChartContent active={active} projectId={projectId} current={current} />
+      </div>
+    </Panel>
+  );
+};
+
+const ListContent = (props) => {
+  const { current, onClick, projectId, active } = props;
+
+  const { data, loading, run } = useRequest(
+    () => {
+      let params = {
+        page: 1,
+        page_size: 99999,
+        project_id: projectId,
+      };
+      return TYPE[active]?.device(params);
+    },
+    {
+      manual: true,
+      onSuccess(data) {
+        if (data.list?.[0]) {
+          onClick(data.list[0]);
+        } else {
+          onClick(null);
+        }
+      },
+    },
+  );
+
+  useEffect(() => {
+    if (active) run();
+  }, [active]);
+
+  return (
+    <div className={styles.listBox}>
+      <div className={styles.title}>设备列表</div>
+      <List
+        split={false}
+        className={styles.list}
+        dataSource={data?.list || []}
+        renderItem={(item, idx) => (
+          <List.Item
+            className={
+              item == current ? styles.listItemActive : styles.listItem
+            }
+            style={
+              idx % 2 == 0
+                ? {
+                    background:
+                      'linear-gradient(to right, rgba(153, 231, 255, 0.1), rgba(96, 168, 255, 0.1))',
+                  }
+                : {
+                    background:
+                      'linear-gradient(to right, rgba(153, 231, 255, 0.2), rgba(96, 168, 255, 0.2))',
+                  }
+            }
+            onClick={() => onClick(item)}
+          >
+            {item.device_code}
+          </List.Item>
+        )}
+      />
+    </div>
+  );
+};
+
+const ChartContent = (props) => {
+  const { current, projectId, active } = props;
+  const [visible, setVisible] = useState(false);
+  const [params, setParams] = useState(null);
+  const [time, setTime] = useState([dayjs().startOf('day'), dayjs()]);
+  const timerRef = useRef({
+    s_time: time[0].format('YYYY-MM-DD HH:mm:ss'),
+    e_time: time[1].format('YYYY-MM-DD HH:mm:ss'),
+  });
+  const domRef = useRef(null);
+  const chartRef = useRef(null);
+
+  const { data, loading, run } = useRequest(
+    () => {
+      let params = {
+        device_code: current.device_code,
+        page: 1,
+        page_size: 9999,
+        project_id: projectId,
+        ...timerRef.current,
+      };
+      return TYPE[active].chart(params);
+    },
+    {
+      manual: true,
+      onSuccess(data) {
+        chartRef.current.clear();
+        let options = getOption(data.list, active);
+        chartRef.current.setOption(options, true);
+        chartRef.current.resize();
+      },
+    },
+  );
+
+  const optimization = useMemo(() => {
+    const result = data?.list?.filter((item) => item.optimization);
+    if (result?.length > 0) {
+      const lastItem = result[result.length - 1].optimization;
+      const valueList = Object.values(lastItem).map((item) => item.remark);
+      return valueList.join(',');
+    }
+    // if (data?.list?.[0]?.optimization?.peb_interval) {
+    //   return data.list[0].optimization.peb_interval.remark;
+    // }
+    return '';
+  }, [data]);
+
+  const searchTime = (type) => {
+    let time = [dayjs().startOf(type), dayjs()];
+    onSearch?.(time);
+  };
+
+  const onSearch = (time) => {
+    if (time && time.length == 2) {
+      let s_time, e_time;
+      s_time = time[0].format('YYYY-MM-DD HH:mm:ss');
+      e_time = time[1].format('YYYY-MM-DD HH:mm:ss');
+
+      timerRef.current = { s_time, e_time };
+      setTime(time);
+      run();
+    }
+  };
+
+  useEffect(() => {
+    chartRef.current = echarts.init(domRef.current);
+
+    return () => {
+      chartRef.current.dispose();
+    };
+  }, []);
+
+  useEffect(() => {
+    if (current?.device_code && active) {
+      run();
+    } else {
+      chartRef.current.clear();
+    }
+  }, [current, active]);
+
+  return (
+    <div className={styles.chartBox}>
+      <div className={styles.title}>
+        优化建议
+        {active == 1 && (
+          <Icon
+            type="area-chart"
+            style={{ float: 'right', lineHeight: '56px', marginRight: 20 }}
+            onClick={() => setVisible(true)}
+          />
+        )}
+      </div>
+      {/* <SearchModule style={{ margin: '20px 0' }} onSearch={onSearch} type={2} /> */}
+      <Form layout="inline" style={{ margin: '10px 0' }}>
+        <Row gutter={24}>
+          <Col span={14}>
+            <Form.Item
+              label="时间"
+              labelCol={{ span: 4 }}
+              wrapperCol={{ span: 20 }}
+            >
+              <RangePicker
+                allowClear={false}
+                value={time}
+                onChange={onSearch}
+              ></RangePicker>
+            </Form.Item>
+          </Col>
+          <Col span={10}>
+            <div>
+              <Button
+                type="primary"
+                onClick={() => searchTime('day')}
+                style={{ marginRight: 10 }}
+              >
+                今日
+              </Button>
+              <Button
+                type="primary"
+                onClick={() => searchTime('week')}
+                style={{ marginRight: 10 }}
+              >
+                本周
+              </Button>
+              <Button type="primary" onClick={() => searchTime('month')}>
+                本月
+              </Button>
+            </div>
+          </Col>
+        </Row>
+      </Form>
+      <Spin spinning={loading}>
+        {!loading && !data?.list && <Empty style={{ height: 195 }} />}
+        <div
+          ref={domRef}
+          style={{ height: '100%', display: !data?.list ? 'none' : 'block' }}
+        />
+      </Spin>
+      {optimization && (
+        <div
+          style={{
+            fontSize: 22,
+            // color: '#fff',
+            textIndent: 30,
+            padding: '10px 0',
+            flexShrink: 1,
+          }}
+        >
+          优化建议:{optimization}
+        </div>
+      )}
+
+      <MembraneModal
+        visible={visible}
+        onCancel={() => setVisible(false)}
+        params={params}
+      />
+    </div>
+  );
+};
+
+function getOption(data = [], active) {
+  let formatter,
+    yAxisName = '',
+    series = [],
+    xAxis = [];
+  var data1 = [],
+    data2 = [],
+    data3 = [],
+    data4 = [];
+  switch (active) {
+    case 'tt_backwash':
+      yAxisName = '清洗周期/Min';
+      formatter = (params) => {
+        let item = data[params[0].dataIndex];
+        let content = '';
+        if (item.bw_type == 1) {
+          // PEB
+          content += 'PEB 反洗开始时间:' + item.peb_st;
+          content += '<br />PEB 反洗结束时间:' + item.peb_et;
+        } else {
+          // CEB
+          content += 'CEB 反洗开始时间:' + item.ceb_st;
+          content += '<br />CEB 反洗结束时间:' + item.ceb_et;
+          content += '<br />CEB 清洗剂浓度' + item.ceb_ppm;
+        }
+        return content;
+      };
+      data?.forEach((item) => {
+        if (item.bw_type == 1) {
+          // 实际冲洗
+          data1.push(Math.ceil(item.peb_interval / 60));
+          // TODO:冲洗
+          data2.push(Math.ceil(item.peb_interval / 60));
+          data3.push(null);
+          data4.push(null);
+          xAxis.push(dayjs(item.peb_st).format('YYYY-MM-DD HH:mm:ss'));
+        } else {
+          data1.push(null);
+          data2.push(null);
+          data3.push(Math.ceil(item.peb_interval / 60));
+          // TODO:模拟冲洗
+          data4.push(Math.ceil(item.peb_interval / 60));
+          xAxis.push(dayjs(item.ceb_st).format('YYYY-MM-DD HH:mm:ss'));
+        }
+      });
+      series = [
+        {
+          name: '物理实际冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '物理模拟冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+        {
+          name: '化学实际冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data3,
+        },
+        {
+          name: '化学模拟冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data4,
+        },
+      ];
+      break;
+    case 'tt_wash':
+      yAxisName = '清洗周期/Min';
+      formatter = (params) => {
+        let item = data[params[0].dataIndex];
+        let content = '';
+        content += '反洗开始时间:' + item.st;
+        content += '<br />反洗结束时间:' + item.et;
+        return content;
+      };
+      data?.forEach((item) => {
+        // 实际冲洗
+        data1.push(Math.ceil(item.interval / 60));
+        // TODO:模拟冲洗
+        data2.push(Math.ceil(item.interval / 60));
+        xAxis.push(dayjs(item.st).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '模拟冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+      ];
+      break;
+    case 'tt_nob':
+      yAxisName = '杀菌周期/Min';
+      formatter = (params) => {
+        let item = data[params[0].dataIndex];
+        let content = '';
+        content += '杀菌开始时间:' + (item.st || '-');
+        content += '<br />杀菌结束时间:' + (item.et || '-');
+        return content;
+      };
+      var data1 = [],
+        data2 = [];
+      data?.forEach((item) => {
+        // 实际冲洗
+        data1.push(Math.ceil(item.interval / 60));
+        // TODO:模拟冲洗
+        data2.push(Math.ceil(item.interval / 60));
+        xAxis.push(dayjs(item.st).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '模拟',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+      ];
+      break;
+    case 'tdr_hci':
+    case 'tdr_nob':
+    case 'tdr_pac':
+      yAxisName = '投加量';
+      // formatter = params => {
+      //   let content = '';
+      //   let item = data[params[0].dataIndex];
+
+      //   content += item.c_time;
+      //   content += '<br />最高加药浓度' + item.dosh + 'g/m3';
+      //   content += '<br />最低加药浓度:' + item.dosl + 'g/m3';
+      //   content += '<br />最高加药浊度:' + item.tubh + 'g/m3';
+      //   content += '<br />最低加药浊度:' + item.tubl + 'g/m3';
+      //   content += '<br />实际进水浊度:' + item.tubr;
+      //   return content;
+      // };
+      data?.forEach((item) => {
+        // 实际冲洗
+        data1.push(Math.ceil(item.fr / 60));
+        // TODO:模拟冲洗
+        data2.push(Math.ceil(item.fcoa / 60));
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际物理投加量',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '理论物理投加量',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+      ];
+      break;
+
+    case 'td_uf':
+    case 'td_mf':
+    case 'td_nf':
+      yAxisName = '渗透率';
+      data?.forEach((item) => {
+        // 实际跨膜压差
+        data1.push(item.permeability);
+        // 模拟跨膜压差
+        data2.push(item.std_permeability);
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际渗透率',
+          type: 'line',
+          data: data1,
+          showSymbol: false,
+        },
+        {
+          name: '模拟渗透率',
+          type: 'line',
+          data: data2,
+          showSymbol: false,
+        },
+      ];
+      break;
+    case 'td_ro':
+      yAxisName = '跨膜压差';
+      data?.forEach((item) => {
+        // 实际跨膜压差
+        data1.push(item.extend['1st_Stage_DP']);
+        data2.push(item.extend['2nd_Stage_DP']);
+        // 模拟跨膜压差
+        data3.push(item.stabilize_extend['1st_Stage_DP']);
+        data4.push(item.stabilize_extend['2nd_Stage_DP']);
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际一段跨膜压差',
+          type: 'line',
+          data: data1,
+          showSymbol: false,
+        },
+        {
+          name: '实际二段跨膜压差',
+          type: 'line',
+          data: data2,
+          showSymbol: false,
+        },
+        {
+          name: '模拟一段跨膜压差',
+          type: 'line',
+          data: data3,
+          showSymbol: false,
+        },
+        {
+          name: '模拟二段跨膜压差',
+          type: 'line',
+          data: data4,
+          showSymbol: false,
+        },
+      ];
+      break;
+  }
+
+  const option = {
+    color: ['#FFC800', '#30EDFD', '#4096ff', '#ff4d4f', '#ffa940'],
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+      formatter,
+    },
+    legend: {
+      textStyle: {
+        // color: '#fff',
+        fontSize: 18,
+      },
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true,
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxis,
+      axisLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      splitLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      axisLabel: {
+        // color: '#fff',
+      },
+    },
+    yAxis: {
+      name: yAxisName,
+      type: 'value',
+      boundaryGap: [0, 0.01],
+      axisLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      splitLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      axisLabel: {
+        // color: '#fff',
+      },
+    },
+    series,
+  };
+
+  return option;
+}
+
+const MembraneModal = (props) => {
+  const { visible, onCancel, params } = props;
+  const domRef = useRef(null);
+  const chartRef = useRef(null);
+
+  const { run, loading } = useRequest(queryMembraneConditions, {
+    manual: true,
+    onSuccess(data) {
+      console.log(data);
+      let options = getMembraneOption(data.list);
+      chartRef.current.setOption(options, true);
+    },
+  });
+
+  useEffect(() => {
+    if (visible) {
+      if (!chartRef.current) {
+        chartRef.current = echarts.init(document.getElementById('chart'));
+        run(params);
+      }
+    }
+  }, [visible]);
+
+  return (
+    <Modal
+      forceRender
+      title="渗透率图表"
+      visible={visible}
+      onCancel={onCancel}
+      footer={null}
+    >
+      <Spin spinning={loading}>
+        <div id="chart" style={{ height: '60vh' }} />
+      </Spin>
+    </Modal>
+  );
+};
+function getMembraneOption(data = []) {
+  const option = {
+    color: ['#FFC800', '#30EDFD', '#4096ff', '#ff4d4f', '#ffa940'],
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+    },
+    legend: {
+      textStyle: {
+        // color: '#fff',
+        fontSize: 18,
+      },
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true,
+    },
+    xAxis: {
+      type: 'category',
+      data: data.map((item) => item.c_time),
+      axisLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      splitLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      axisLabel: {
+        // color: '#fff',
+      },
+    },
+    yAxis: {
+      name: '渗透率',
+      type: 'value',
+      boundaryGap: [0, 0.01],
+      axisLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      splitLine: {
+        lineStyle: {
+          // color: '#fff',
+        },
+      },
+      axisLabel: {
+        // color: '#fff',
+      },
+    },
+    series: [
+      {
+        type: 'line',
+        showSymbol: false,
+        areaStyle: {
+          opacity: 0.1,
+        },
+        type: 'line',
+        smooth: true,
+        data: data.map((v) => v.permeability),
+      },
+    ],
+  };
+
+  return option;
+}
+
+export default SimulateDetail;

+ 102 - 0
src/pages/Smart/components/SimulateDetail.less

@@ -0,0 +1,102 @@
+.box {
+  display: flex;
+  height: 54.2vh;
+}
+
+.title {
+  padding-left: 20px;
+  height: 56px;
+  width: 100%;
+  font-size: 24px;
+  // color: #fff;
+  line-height: 56px;
+  // background: url('@/assets/newUI/theadBg.png') no-repeat center;
+  background-size: 100% 100%;
+}
+
+.listBox {
+  width: 33%;
+  height: 100%;
+  margin-right: 40px;
+  display: flex;
+  flex-direction: column;
+  max-width: 220px;
+  :global {
+    .ant-empty {
+      margin-left: -30px;
+    }
+  }
+}
+
+.list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.listItem {
+  display: block !important;
+  text-align: center;
+  margin-bottom: 5px;
+  // color: #fff;
+  font-size: 20px;
+  cursor: pointer;
+  border-bottom: 1px solid rgba(153, 231, 255, 0.4);
+}
+
+.listItemActive {
+  .listItem;
+  background: #40a9ff !important;
+}
+
+.tabs {
+  display: flex;
+  width: 72%;
+  align-items: center;
+  .item {
+    padding: 4px 20px;
+    border: 1px solid #02a7f0;
+    border-left: none;
+    font-size: 20px;
+    // color: #fff;
+    cursor: pointer;
+    max-height: 40px;
+    width: 100%;
+    white-space: nowrap;
+    &:nth-child(1) {
+      border-left: 1px solid #02a7f0;
+    }
+
+    &.active {
+      background-color: #02a7f0;
+    }
+  }
+  .left {
+    // background: url('@/assets/newUI/left.png') no-repeat center;
+    background-size: 100% 100%;
+    width: 31px;
+    height: 32px;
+    margin-right: 12px;
+    cursor: pointer;
+  }
+
+  .right {
+    // background: url('@/assets/newUI/right.png') no-repeat center;
+    background-size: 100% 100%;
+    width: 31px;
+    height: 32px;
+    margin-left: 12px;
+    cursor: pointer;
+  }
+  .content {
+    display: flex;
+    width: 90%;
+    overflow-x: hidden;
+  }
+}
+
+.chartBox {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}

+ 117 - 0
src/pages/Smart/components/SimulatePie.js

@@ -0,0 +1,117 @@
+import * as echarts from 'echarts';
+import React, { useEffect, useRef, useState } from 'react';
+import Panel from './Panel';
+import { querySimulationProfit } from '@/services/SmartOps';
+import { history, useParams, useRequest } from '@umijs/max';
+import dayjs from 'dayjs';
+import { Button } from 'antd';
+
+const SimulatePie = props => {
+  const { projectId } = props;
+  const domRef = useRef(null);
+  const chartRef = useRef(null);
+
+  const { data } = useRequest(querySimulationProfit, {
+    defaultParams: [
+      {
+        project_id: projectId,
+        s_time: dayjs()
+          .subtract(1, 'day')
+          .format('YYYY-MM-DD HH:mm:ss'),
+        e_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      },
+    ],
+    onSuccess(data) {
+      console.log(data);
+      let options = getOption(data);
+
+      // chartRef.current.clear();
+      chartRef.current.setOption(options);
+    },
+  });
+
+  const getProfit = () => {
+    if (!data?.info) return '-';
+
+    return Object.values(data.info).reduce((total, currentValue) => total + currentValue, 0);
+  };
+
+  useEffect(() => {
+    chartRef.current = echarts.init(domRef.current);
+
+    // 在组件卸载时销毁图表实例
+    return () => {
+      chartRef.current.dispose();
+    };
+  }, []);
+
+  return (
+    <Panel
+      title={'模拟预估'}
+      style={{ width: '100%' }}
+      btns={
+        <Button type="primary" onClick={() => history.go(-1)}>
+          返回
+        </Button>
+      }
+    >
+      <h2 style={{ textAlign: 'center', fontSize: 24, }}>
+        通过模拟仿真预计未来一日可省 &nbsp;
+        <span style={{ fontSize: 28, color: '#FFFF00' }}>{getProfit()}元</span>
+      </h2>
+      <div ref={domRef} style={{ height: '25vh' }}></div>
+    </Panel>
+  );
+};
+
+function getOption(data) {
+  let seriesData = [];
+  let type = {
+    0: '未分类',
+    1: '过滤周期优化',
+    2: '絮凝剂投加优化',
+    3: '膜寿命预测',
+    4: '膜反冲洗建议',
+    5: '膜化学清洗建议',
+    6: 'RO一段优化建议',
+    7: 'RO二段优化建议',
+    8: 'RO三段优化建议',
+    9: 'RO杀菌周期优化',
+    10: 'RO冲洗周期优化',
+  };
+  Object.entries(data.info).map(([key, value]) => {
+    if(value > 0) {
+      seriesData.push({
+        value,
+        name: type[key],
+      });
+    }
+  });
+  let option = {
+    color: ['#30EDFD', '#FFC800'],
+    tooltip: {
+      trigger: 'item',
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['50%', '70%'],
+        label: {
+          fontSize: 22,
+        },
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+        data: seriesData,
+      },
+    ],
+  };
+
+  return option;
+}
+
+export default SimulatePie;

+ 6 - 6
src/pages/Smart/index.js

@@ -33,7 +33,7 @@ const Work = (props) => {
               type="primary"
               onClick={() =>
                 history.push(
-                  `/unity/smart/work/optimization-tasks/${projectId}?score=${data.score}`,
+                  `/smart/optimization-tasks/${projectId}?score=${data.score}`,
                 )
               }
             >
@@ -45,19 +45,19 @@ const Work = (props) => {
             style={{ marginRight: 20 }}
             type="primary"
             onClick={() =>
-              history.push('/unity/smart/work/condition-detection/' + projectId)
+              history.push('/smart/condition-detection/' + projectId)
             }
           >
             工况检测
           </Button>
-          <Button
+          {/* <Button
             type="primary"
             onClick={() =>
-              history.push(`/unity/smart/work/${projectId}/params`)
+              history.push(`/smart/params/${projectId}`)
             }
           >
             工况模拟
-          </Button>
+          </Button> */}
         </div>
       </div>
       <Row gutter={16}>
@@ -101,7 +101,7 @@ const Work = (props) => {
                 <i></i>外供水浊度:{data?.dtur}
               </li>
               <li>
-                <i></i>外供水余:{data?.dsan}
+                <i></i>外供水余:{data?.dsan}
               </li>
             </ul>
           </div>

+ 4 - 4
src/pages/Smart/index.less

@@ -1,6 +1,6 @@
 .score {
   margin: 20px 0;
-  color: #fff;
+  // color: #fff;
   display: flex;
   align-items: center;
   padding-left: 60px;
@@ -11,7 +11,7 @@
   margin-left: 30px;
 
   h3 {
-    color: #fff;
+    // color: #fff;
     font-size: 24px;
     margin-bottom: 14px;
   }
@@ -23,7 +23,7 @@
 }
 
 .card {
-  color: #fff;
+  // color: #fff;
   border-radius: 8px;
   // box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.8);
   padding: 20px;
@@ -34,7 +34,7 @@
   background-size: 100% 100%;
 
   h3 {
-    color: #fff;
+    // color: #fff;
     font-size: 24px;
     margin-bottom: 20px;
   }

+ 22 - 14
src/services/SmartOps.js

@@ -1,6 +1,14 @@
 import { request } from 'umi';
 import { stringify } from 'qs';
 
+export async function queryProcessSection(projectId) {
+  const res = await request(`/api/v1/process-section/${projectId}?page_size=999`);
+  return res?.data?.list;
+}
+
+export async function queryUserList(param) {
+  return request(`/api/v1/user/project/${param.projectId}`)
+}
 /**
  * 最优工况列表
  * @param {*} data.project_id
@@ -11,7 +19,7 @@ export async function queryBaseList(data) {
   let res = await request(`/api/smart/v1/base/list`, {
     method: 'POST',
     dataType: 'formData',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -26,7 +34,7 @@ export async function queryOptimum(data) {
   let res = await request(`/api/smart/v1/optimum-work/list`, {
     method: 'POST',
     dataType: 'formData',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -44,7 +52,7 @@ export async function addOptimum(data) {
   let res = await request(`/api/smart/v1/optimum-work/add`, {
     method: 'POST',
     dataType: 'formData',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -60,7 +68,7 @@ export async function addBaseParams(data) {
   let res = await request(`/api/smart/v1/base/add`, {
     method: 'POST',
     dataType: 'formData',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -78,7 +86,7 @@ export async function editOptimum(data) {
   let res = await request(`/api/smart/v1/optimum-work/edit`, {
     method: 'POST',
     dataType: 'formData',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -87,7 +95,7 @@ export async function updateBase(data) {
   let res = await request(`/api/smart/v1/base/edit`, {
     method: 'POST',
     dataType: 'formData',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -133,7 +141,7 @@ export async function queryPacDosing(data) {
 export async function addPac(data) {
   let res = await request(`/api/simulations/v1/pac`, {
     method: 'POST',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -152,7 +160,7 @@ export async function addPac(data) {
 export async function updatePac(data) {
   let res = await request(`/api/simulations/v1/pac`, {
     method: 'PUT',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -160,7 +168,7 @@ export async function updatePac(data) {
 export async function conditionChart(params) {
   let res = await request(`/api/energy/v1/condition/chart`, {
     method: 'POST',
-    body: params,
+    data: params,
   });
   return res
 }
@@ -220,7 +228,7 @@ export async function queryBackwash(data) {
 export async function addBackwash(data) {
   let res = await request(`/api/simulations/v1/backwash`, {
     method: 'POST',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -237,7 +245,7 @@ export async function addBackwash(data) {
 export async function updateBackwash(data) {
   let res = await request(`/api/simulations/v1/pac`, {
     method: 'PUT',
-    body: data,
+    data: data,
   });
   return res
 }
@@ -256,7 +264,7 @@ export async function querySimulationProfit(data) {
 export async function conditionEstimate(params) {
   let res = await request(`/api/energy/v1/condition/estimate`, {
     method: 'POST',
-    body: params,
+    data: params,
   });
   return res
 }
@@ -295,7 +303,7 @@ export async function queryConditionSnapshot(data) {
 export async function queryRealEstimate(project_id) {
   let res = await request(`/api/energy/v1/condition/real-estimate`, {
     method: 'POST',
-    body: {
+    data: {
       project_id,
     },
   });
@@ -306,7 +314,7 @@ export async function queryRealEstimate(project_id) {
 export async function queryRealEstimateChart(project_id) {
   let res = await request(`/api/energy/v1/condition/real-chart`, {
     method: 'POST',
-    body: {
+    data: {
       project_id,
       hour: 24
     },

+ 75 - 0
src/services/eqSelfInspection.js

@@ -0,0 +1,75 @@
+import { stringify } from 'qs';
+import { request } from 'umi';
+
+//获取指定路线巡检结果
+export async function getRecentAutoPatrolByRouteId(params) {
+  return request(`/api/v1/patrol/auto/data/${params.projectId}?${stringify(params)}`);
+}
+export async function getAutoPatrolByRouteId(params) {
+  return request(`/api/v1/patrol/data/${params.projectId}/${params.routeId}`);
+}
+export async function getDumuDetail(detailId) {
+  const res = await request(`/api/v1/dumu/detail/${detailId}`);
+  return res.data
+}
+
+export async function queryUserList(param) {
+  return request(`/api/v1/user/project/${param.projectId}`)
+}
+export async function queryAnalysisDict() {
+  const res = await request(
+    `/api/analysis/v1/analysis-dict/list?page_size=9999`,
+    {
+      method: 'POST',
+    },
+  );
+  return { data: res?.data?.list };
+}
+
+export async function changeRecordStatus(params) {
+  return request('/api/v1/patrol/record/item', {
+    method: 'PUT',
+    data: {
+      ...params,
+    },
+  });
+}
+
+export async function getRouteList(params) {
+  return request(`/api/v1/patrol/route-info/${params.ProjectId}`);
+}
+
+//获取指定路线巡检结果
+export async function getPatrolRecordMandateInfo(params) {
+  return request(`/api/v1/mandate/info?${stringify(params)}`);
+}
+export async function queryPatrolRecord(params) {
+  return request(`/api/v1/patrol/record/${params.recordId}`);
+}
+export async function analysisResultList(params) {
+  return request(`/api/v1/patrol/analysis-result?${stringify(params)}`);
+}
+export async function patrolRelationList(params) {
+  return request(`/api/analysis/v1/patrol-relation/list`, {
+    method: 'POST',
+    data: params,
+  });
+}
+export async function getPatrolDumuList(params) {
+  return request(`/api/v1/dumu/patrol-list?${stringify(params)}`, {
+    method: 'GET',
+  });
+}
+export async function patrolOverview(params) {
+  return request(`/api/v1/patrol/overview/${params.projectId}`);
+}
+
+export async function patrolOverviewLine(params) {
+  return request(
+    `/api/v1/patrol/chart-curve/${params.projectId}?${stringify(params)}`,
+  );
+}
+
+export async function patrolOverviewPie(params) {
+  return request(`/api/v1/patrol/chart-pie/${params.projectId}?${stringify(params)}`);
+}

+ 0 - 21
src/utils/utils.js

@@ -144,27 +144,6 @@ export const UnityAction = {
     }
   },
 };
-
-// if (window.vuplex) {
-//   window.vuplex.addEventListener('message', e => {
-//     console.log('============================getMessageForUnity============================');
-//     const data = JSON.parse(e.data);
-//     console.log(data);
-//     UnityAction.emit(data.type, data.message);
-
-//     // 将消息广播给子页面
-//     // let iframeDom = document.getElementsByClassName('iframe');
-//     // for (let i = 0; i < iframeDom.length; i++) {
-//     //   const item = iframeDom[i];
-//     //   item.contentWindow.postMessage(data.type, data.message);
-//     // }
-//   });
-// }
-
-// export const EventBus = new MessageEvent();
-// export const UnityAction = new UnityMessageEvent();
-// export const PageAction = new PageMessageEvent();
-
 export function getGlobalData(key) {
   let data;
   try {

+ 66 - 10
yarn.lock

@@ -72,7 +72,7 @@
     lodash "^4.17.15"
     rc-util "^5.9.4"
 
-"@ant-design/icons@^5.0.0", "@ant-design/icons@^5.0.1", "@ant-design/icons@^5.2.2":
+"@ant-design/icons@^5.0.0", "@ant-design/icons@^5.2.2":
   version "5.2.5"
   resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.2.5.tgz#852474359e271a36e54a4ac115065fae7396277e"
   integrity sha512-9Jc59v5fl5dzmxqLWtRev3dJwU7Ya9ZheoI6XmZjZiQ7PRtk77rC+Rbt7GJzAPPg43RQ4YO53RE1u8n+Et97vQ==
@@ -4261,6 +4261,14 @@ eastasianwidth@^0.2.0:
   resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
   integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
 
+echarts@^5.4.3:
+  version "5.4.3"
+  resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c"
+  integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==
+  dependencies:
+    tslib "2.3.0"
+    zrender "5.4.4"
+
 electron-to-chromium@^1.4.477:
   version "1.4.492"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.492.tgz#83fed8beb64ec60578069e15dddd17b13a77ca56"
@@ -6122,6 +6130,11 @@ lodash.debounce@^4.0.8:
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
   integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
 
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
+
 lodash.merge@^4.6.2:
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -7227,7 +7240,7 @@ process@^0.11.10:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
 
-prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -7283,20 +7296,13 @@ qrcode.react@^3.1.0:
   resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
   integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
 
-qs@^6.11.0:
+qs@^6.11.0, qs@^6.11.2:
   version "6.11.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
   integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
   dependencies:
     side-channel "^1.0.4"
 
-qs@^6.11.2:
-  version "6.11.2"
-  resolved "https://registry.npmmirror.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
-  integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
-  dependencies:
-    side-channel "^1.0.4"
-
 query-string@^6.13.6:
   version "6.14.1"
   resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a"
@@ -8015,6 +8021,16 @@ react-dom@18.1.0:
     loose-envify "^1.1.0"
     scheduler "^0.22.0"
 
+react-dom@^16.6.3:
+  version "16.14.0"
+  resolved "https://registry.npmmirror.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
+  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    scheduler "^0.19.1"
+
 react-error-overlay@6.0.9:
   version "6.0.9"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
@@ -8106,6 +8122,17 @@ react-router@6.3.0:
   dependencies:
     history "^5.2.0"
 
+react-zmage@^0.8.5:
+  version "0.8.5"
+  resolved "https://registry.npmmirror.com/react-zmage/-/react-zmage-0.8.5.tgz#ac9ccf694e854bbbffda9a0934d0cf7d976cc882"
+  integrity sha512-HGMWf3iPwtW9yiUQlAViap+UtwJiBxzwRi+xJXiNYoGFRpAeTKUnAK5qiVeXcPGp1mrkrmnqJTIy49sAX4iRqg==
+  dependencies:
+    classnames "^2.2.6"
+    lodash.memoize "^4.1.2"
+    prop-types "^15.6.2"
+    react "^16.6.3"
+    react-dom "^16.6.3"
+
 react@18.1.0:
   version "18.1.0"
   resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
@@ -8113,6 +8140,15 @@ react@18.1.0:
   dependencies:
     loose-envify "^1.1.0"
 
+react@^16.6.3:
+  version "16.14.0"
+  resolved "https://registry.npmmirror.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
+  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+
 reactcss@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
@@ -8444,6 +8480,14 @@ sax@^1.2.4:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
+scheduler@^0.19.1:
+  version "0.19.1"
+  resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
+  integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
 scheduler@^0.22.0:
   version "0.22.0"
   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
@@ -9147,6 +9191,11 @@ trim-newlines@^3.0.0:
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
   integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
+tslib@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
+  integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
+
 tslib@^1.8.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -9621,3 +9670,10 @@ yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zrender@5.4.4:
+  version "5.4.4"
+  resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261"
+  integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==
+  dependencies:
+    tslib "2.3.0"