瀏覽代碼

增加智慧分析模块

Renxy 1 年之前
父節點
當前提交
60dbd66cfa
共有 33 個文件被更改,包括 3084 次插入38 次删除
  1. 35 11
      .umirc.ts
  2. 二進制
      src/assets/current.png
  3. 二進制
      src/assets/smartOps/icon01.png
  4. 二進制
      src/assets/smartOps/icon02.png
  5. 二進制
      src/assets/smartOps/icon03.png
  6. 二進制
      src/assets/smartOps/icon04.png
  7. 二進制
      src/assets/smartOps/icon05.png
  8. 二進制
      src/assets/smartOps/icon06.png
  9. 二進制
      src/assets/smartOps/icon07.png
  10. 二進制
      src/assets/tbodyBg1.png
  11. 195 0
      src/pages/SmartOps/Analysis.js
  12. 216 0
      src/pages/SmartOps/ChartPage.js
  13. 198 0
      src/pages/SmartOps/HistoryRecord.js
  14. 229 0
      src/pages/SmartOps/OperationRecord.js
  15. 822 0
      src/pages/SmartOps/WorkAnalysisDetail.js
  16. 53 0
      src/pages/SmartOps/WorkAnalysisDetail.less
  17. 61 0
      src/pages/SmartOps/components/BarModal.js
  18. 28 0
      src/pages/SmartOps/components/BarModal.less
  19. 52 0
      src/pages/SmartOps/components/ThresholdBar.js
  20. 46 0
      src/pages/SmartOps/components/ThresholdBar.less
  21. 142 0
      src/pages/SmartOps/components/VideoAnalysis.js
  22. 72 0
      src/pages/SmartOps/components/VideoAnalysis.less
  23. 50 0
      src/pages/SmartOps/components/WorkAnalysis.js
  24. 42 0
      src/pages/SmartOps/components/WorkAnalysis.less
  25. 375 0
      src/pages/SmartOps/index.js
  26. 173 0
      src/pages/SmartOps/index.less
  27. 88 0
      src/pages/SmartOps/models/smartOps.js
  28. 49 0
      src/services/DeviceInfo.js
  29. 3 3
      src/services/OperationManagement.js
  30. 59 20
      src/services/SmartOps.js
  31. 4 4
      src/services/TaskManage.js
  32. 37 0
      src/services/diagnosticTec.js
  33. 55 0
      src/services/dumu.js

+ 35 - 11
.umirc.ts

@@ -18,7 +18,11 @@ export default defineConfig({
     { 'http-equiv': 'expires', content: '0' },
     { 'http-equiv': 'X-UA-Compatible', content: 'IE=EmulateIE9' },
     { name: 'transparent', content: 'true' },
-    { name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' },
+    {
+      name: 'viewport',
+      content:
+        'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0',
+    },
   ],
   proxy: {
     '/api': {
@@ -178,16 +182,36 @@ export default defineConfig({
       path: '/safety/detail/:projectId', ///safety management
       component: './SafetyManagement/doorDetail',
     },
-    // {
-    //   name: '权限演示',
-    //   path: '/access',
-    //   component: './Access',
-    // },
-    // {
-    //   name: ' CRUD 示例',
-    //   path: '/table',
-    //   component: './Table',
-    // },
+    // 智慧运营
+    {
+      name: '',
+      path: '/smart-ops/:projectId', ///safety management
+      component: './SmartOps/index',
+    },
+    // 工况分析详情
+    {
+      name: '',
+      path: '/smart-ops/work-analysis-detail/:projectId', ///safety management
+      component: './SmartOps/WorkAnalysisDetail',
+    },
+    // 历史记录
+    {
+      name: '',
+      path: '/smart-ops/history-record/:projectId', ///safety management
+      component: './SmartOps/HistoryRecord',
+    },
+    // 操作记录
+    {
+      name: '',
+      path: '/smart-ops/operation-record/:projectId', ///safety management
+      component: './SmartOps/OperationRecord',
+    },
+    // 图标弹窗页面
+    {
+      name: '',
+      path: '/smart-ops/chart-page/:projectId', ///safety management
+      component: './SmartOps/ChartPage',
+    },
   ],
   npmClient: 'yarn',
 });

二進制
src/assets/current.png


二進制
src/assets/smartOps/icon01.png


二進制
src/assets/smartOps/icon02.png


二進制
src/assets/smartOps/icon03.png


二進制
src/assets/smartOps/icon04.png


二進制
src/assets/smartOps/icon05.png


二進制
src/assets/smartOps/icon06.png


二進制
src/assets/smartOps/icon07.png


二進制
src/assets/tbodyBg1.png


+ 195 - 0
src/pages/SmartOps/Analysis.js

@@ -0,0 +1,195 @@
+import { UnityAction } from '@/utils/utils';
+import { connect } from '@umijs/max';
+import { Button, Spin, Table, Tabs } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import ThresholdBar from './components/ThresholdBar';
+import styles from './index.less';
+const { TabPane } = Tabs;
+
+const Analysis = (props) => {
+  const Status = [
+    { name: '异常', type: '1', data: [] },
+    { name: '告警', type: '2', data: [] },
+    { name: '提醒', type: '3', data: [] },
+  ];
+
+  const { list = [], processList, loading } = props;
+  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+  const [tab, setTab] = useState('1');
+
+  const columns = [
+    {
+      title: '设备名称',
+      width: '20%',
+      render: (record) => (
+        <div>
+          {record.DeviceName}({record.DeviceCode})
+        </div>
+      ),
+    },
+    {
+      title: '工艺单元',
+      width: '14%',
+      dataIndex: 'ProcessSectionId',
+      render: (id) => {
+        return processList.find((item) => item.id == id)?.name;
+      },
+    },
+    {
+      title: '阈值范围',
+      width: '14%',
+      render: (record) => {
+        // let thresholds = [-3,4,6,1];
+        let thresholds = record.fault_range.split(',');
+        return (
+          <div>
+            {record.fault_range && (
+              <ThresholdBar
+                current={record.fault_result}
+                thresholds={thresholds}
+              />
+            )}
+            {record.plcs && (
+              <Button
+                type="primary"
+                className={styles.anaBtn}
+                onClick={() =>
+                  handleBtnClick(
+                    record.plcs,
+                    record.DeviceName || record.DeviceCode,
+                  )
+                }
+              >
+                相关性分析
+              </Button>
+            )}
+          </div>
+        );
+      },
+    },
+    {
+      title: '可能原因',
+      width: '20%',
+      dataIndex: 'Reason',
+    },
+    {
+      title: '解决方案',
+      render: (record) => {
+        if (record.FixPlan instanceof Array) {
+          return (
+            <div>
+              {record.FixPlan.map((item) => (
+                <div>
+                  {item.content}
+                  <br />
+                </div>
+              ))}
+            </div>
+          );
+        } else {
+          return record.FixPlan;
+        }
+      },
+    },
+  ];
+
+  useEffect(() => {
+    UnityAction.on('SynDev', selectedItem);
+    return () => UnityAction.off('SynDev', selectedItem);
+  }, [data, tab]);
+
+  const selectedItem = (e) => {
+    setSelectedRowKeys([e]);
+    console.log(data);
+    const itemIndex = data?.findIndex((item) => item.type == tab);
+    const index = data[itemIndex]?.data?.findIndex(
+      (item) => item.DeviceCode == e,
+    );
+    if (index !== 0 || index != -1) {
+      const dom = document.querySelector(`tr[data-row-key="${index}"]`);
+      if (dom) {
+        let v = document.getElementsByClassName('ant-table-body')[itemIndex];
+        v.scrollTop = dom.offsetTop;
+      }
+    }
+  };
+
+  const data = useMemo(() => {
+    return Status.map((item, idx) => {
+      const data = list?.filter((cur) => cur.grade_alarm == item.type);
+      return { ...item, data };
+    });
+  }, [list]);
+
+  const handleBtnClick = (plcs, title) => {
+    if (!plcs) return;
+    const msg = JSON.stringify(plcs); //[{ dataitemid: 'Raw_Water_Tank_Level', deviceid: '1436022785' }]
+    UnityAction.sendMsg(
+      'ProcessAnalysisAbout',
+      JSON.stringify({
+        title,
+        data: msg,
+      }),
+    );
+  };
+
+  const onTabChange = (tab) => {
+    setTab(tab);
+    UnityAction.sendMsg('ProcessAnalysisType', tab);
+  };
+
+  const rowSelection = {
+    type: 'radio',
+    selectedRowKeys,
+    onChange: (selectedRowKeys, selectedRows) => {
+      setSelectedRowKeys(selectedRowKeys);
+      UnityAction.sendMsg('SynDev', selectedRows[0].DeviceCode);
+    },
+  };
+
+  const onSelectRow = (record, index) => {
+    const selectedList = [...selectedRowKeys];
+    if (selectedList[0] === index) return;
+    selectedList[0] = index;
+    setSelectedRowKeys(selectedList);
+    UnityAction.sendMsg('SynDev', record.DeviceCode);
+  };
+
+  const setRowClassName = (record, index) => {
+    return index === selectedRowKeys[0] ||
+      record.DeviceCode == selectedRowKeys[0]
+      ? styles.tableSelect
+      : '';
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <Tabs defaultActiveKey="1" onChange={onTabChange}>
+        {data?.map((item) => (
+          <TabPane
+            tab={`${item.name}(${item.data?.length || 0})`}
+            key={item.type}
+          >
+            <Table
+              dataSource={item.data}
+              columns={columns}
+              // rowKey={'DeviceCode'}
+              // rowSelection={rowSelection}
+              rowClassName={setRowClassName}
+              onRow={(record, index) => ({
+                onClick: () => onSelectRow(record, index),
+              })}
+              pagination={false}
+              scroll={{ y: document.body.clientHeight - 542 }}
+            />
+          </TabPane>
+        ))}
+      </Tabs>
+    </Spin>
+  );
+};
+export default connect(({ smartOps, loading }) => ({
+  loading: loading.effects['smartOps/queryList'],
+  list: smartOps.list.list,
+  processList: smartOps.processList,
+}))(Analysis);

+ 216 - 0
src/pages/SmartOps/ChartPage.js

@@ -0,0 +1,216 @@
+import ChartModule from '@/components/ManagementPage/chartModule';
+import PageContent from '@/components/PageContent';
+import { getDeviceRealDataByTime } from '@/services/SmartOps';
+import { useParams, useSearchParams } from '@umijs/max';
+import { Button, DatePicker, Empty, Form, Spin } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+const tabs = [
+  {
+    label: '近一天',
+    value: 24,
+    timeRange: [dayjs().subtract(1, 'day'), dayjs()],
+  },
+  {
+    label: '近一周',
+    value: 24 * 7,
+    timeRange: [dayjs().subtract(7, 'day'), dayjs()],
+  },
+  {
+    label: '近一个月',
+    value: 24 * 30,
+    timeRange: [dayjs().subtract(1, 'month'), dayjs()],
+  },
+];
+
+const ChartPage = (props) => {
+  const {
+    // location: {
+    //   query: { type = 2, data },
+    // },
+  } = props;
+
+  const { projectId } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const type = searchParams.get('type') || 2;
+  const data = searchParams.get('data');
+
+  const [loading, setLoading] = useState(false);
+  const [options, setOptions] = useState({});
+  const [timeActive, setTimeActive] = useState(0);
+  const [timeRange, setTimeRange] = useState([
+    dayjs().subtract(1, 'day'),
+    dayjs(),
+  ]);
+
+  const defaultSearch = {
+    deviceid: '',
+    dataitemid: '',
+    project_id: projectId,
+    stime: dayjs().subtract(1, 'day').valueOf(),
+    etime: dayjs().valueOf(),
+    size: 10,
+    interval: 'minute',
+    aggregator: 'realtime',
+  };
+
+  const changeTime = async (timeType, date) => {
+    const values = { ...defaultSearch };
+    let diffDay;
+    // values.timeType = timeType;
+    if (date) {
+      const [startDate, endDate] = date;
+      values.etime = endDate * 1;
+      values.stime = startDate * 1;
+      diffDay = endDate.diff(startDate, 'days');
+    } else {
+      let currentDate = dayjs();
+      values.etime = currentDate * 1;
+      values.stime = currentDate.subtract(timeType, 'hour') * 1;
+      diffDay = (timeType / 24).toFixed(0);
+    }
+    if (diffDay >= 15 && diffDay < 30) {
+      // 15-30天 时间间隔不能为分钟
+      values.interval = 'h';
+      values.size = 1;
+    } else if (diffDay >= 30) {
+      // 超过30天 时间间隔只能为天
+      values.interval = 'day';
+      values.size = 1;
+    }
+
+    setLoading(true);
+    const list = JSON.parse(data);
+    const paramsList = list?.map((item) => {
+      return { ...values, ...item }; // deviceid: item.deviceid, dataitemid: item.dataitemid
+    });
+
+    getData(paramsList);
+  };
+
+  const handleTabChange = (tab) => {
+    const params = { ...defaultSearch };
+    switch (tab) {
+      case 1: //日
+        break;
+      case 2: //月
+        params.size = 1;
+        params.interval = 'h';
+        params.stime = dayjs().subtract(1, 'month').valueOf();
+        params.etime = dayjs().valueOf();
+        break;
+      case 3: //年
+        params.size = 1;
+        params.interval = 'day';
+        params.stime = dayjs().subtract(1, 'year').valueOf();
+        params.etime = dayjs().valueOf();
+        break;
+    }
+
+    const list = JSON.parse(data);
+    const paramsList = list?.map((item) => {
+      return {
+        ...params,
+        deviceid: item.deviceid,
+        dataitemid: item.dataitemid,
+      };
+    });
+    getData(paramsList);
+  };
+
+  useEffect(() => {
+    handleTabChange(1);
+  }, []);
+
+  const getData = async (list) => {
+    const data = await Promise.all(
+      list?.map((item) => {
+        return getDeviceRealDataByTime(item).then((res) => res.data);
+      }),
+    );
+    setLoading(false);
+    const options = {
+      yName: '',
+      xData: data.length > 0 && data[0]?.map((item) => item.htime_at),
+      dataList: data?.map((item) => {
+        return {
+          type: 0,
+          name: item?.[0]?.name,
+          data: item?.map((item) => item.val * 1),
+        };
+      }),
+    };
+    // console.log(options);
+
+    if (
+      options.dataList?.every((item) => !item.data || item.data.length == 0)
+    ) {
+      setOptions(null);
+    } else {
+      setOptions(options);
+    }
+  };
+
+  return (
+    <PageContent>
+      <div
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'space-between',
+        }}
+      >
+        <div className={styles.form}>
+          <Form.Item style={{ marginBottom: 0 }}>
+            <RangePicker
+              value={timeRange}
+              showTime={false}
+              disabledDate={(current) =>
+                current && current > dayjs().endOf('day')
+              }
+              onChange={(date) => {
+                setTimeRange(date);
+                setTimeActive(null);
+                changeTime(-1, date);
+              }}
+              allowClear={false}
+            />
+          </Form.Item>
+          <div>
+            {tabs.map((item, index) => (
+              <Button
+                key={item.type}
+                type="primary"
+                style={{
+                  marginRight: 20,
+                  background: timeActive == index ? '#329BFE' : '#2F4D83',
+                }}
+                onClick={() => {
+                  setTimeActive(index);
+                  changeTime(item.value);
+                  setTimeRange(item.timeRange);
+                }}
+              >
+                {item.label}
+              </Button>
+            ))}
+          </div>
+        </div>
+      </div>
+
+      <Spin spinning={loading}>
+        <div style={{ height: 'calc(100vh - 100px)' }}>
+          {options ? (
+            <ChartModule chartType="line" {...options} />
+          ) : (
+            <Empty style={{ marginTop: 140 }} />
+          )}
+        </div>
+      </Spin>
+    </PageContent>
+  );
+};
+export default ChartPage;

+ 198 - 0
src/pages/SmartOps/HistoryRecord.js

@@ -0,0 +1,198 @@
+import PageContent from '@/components/PageContent';
+import { getHistoryRecord } from '@/services/SmartOps';
+import { GetTokenFromUrl } from '@/utils/utils';
+import { useNavigate, useParams, useRequest } from '@umijs/max';
+import { Button, DatePicker, Select, Spin, Table } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+const { Option } = Select;
+
+const HistoryRecord = (props) => {
+  const { projectId } = useParams();
+  const navigate = useNavigate();
+
+  const convertObject2FormData = (params) => {
+    const formData = new FormData();
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== null && value !== undefined && value !== NaN) {
+        formData.append(key, value);
+      }
+    });
+    return formData;
+  };
+
+  const defaultParams = {
+    project_id: Number(projectId),
+    start_time: '',
+    end_time: '',
+    page: 1,
+    page_size: 10,
+  };
+
+  const [queryParams, setQueryParams] = useState(defaultParams);
+
+  const [formData, setFormData] = useState(
+    convertObject2FormData(defaultParams),
+  );
+
+  const {
+    data,
+    run: getList,
+    loading,
+  } = useRequest((params = formData) => getHistoryRecord(params), {
+    formatResult: (res) => {
+      return res?.data;
+    },
+  });
+
+  const columns = [
+    {
+      title: '时间',
+      dataIndex: 'CTime',
+      key: 'CTime',
+      render: (text) => {
+        return dayjs(text).format('YYYY-MM-DD HH:mm') || '--';
+      },
+    },
+    {
+      title: '工况分析',
+      dataIndex: 'Num1',
+      key: 'Num1',
+      render: (text) => {
+        if (text === undefined || text === null) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '工艺分析',
+      dataIndex: 'Num2',
+      key: 'Num2',
+      render: (text) => {
+        if (text === undefined || text === null) {
+          return '--';
+        }
+        if (String(text).includes(',')) {
+          return text.split(',').length;
+        }
+        return text;
+      },
+    },
+    {
+      title: '感知分析',
+      dataIndex: 'Num3',
+      key: 'Num3',
+      render: (text) => {
+        if (text === undefined || text === null) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '操作',
+      render: (record) => (
+        <a
+          onClick={() => {
+            navigate(
+              `/smart-ops/${projectId}?time=${record.CTime}&idList=${
+                record.Num2
+              }&JWT-TOKEN=${GetTokenFromUrl()}`,
+            );
+          }}
+        >
+          详情
+        </a>
+      ),
+    },
+  ];
+
+  const handleParamsChange = (key, value) => {
+    const tempParams = {
+      project_id: Number(projectId),
+      start_time: queryParams.start_time || '',
+      end_time: queryParams.end_time || '',
+      page: queryParams.page || 1,
+      page_size: queryParams.page_size || 10,
+    };
+    switch (key) {
+      case 'date':
+        if (value.length === 2) {
+          tempParams.start_time = dayjs(value[0]).format('YYYY-MM-DD 00:00:00');
+          tempParams.end_time = dayjs(value[1]).format('YYYY-MM-DD 23:59:59');
+          console.log(
+            '----------------',
+            dayjs(value[0]).format('YYYY-MM-DD 00:00:00'),
+            dayjs(value[1]).format('YYYY-MM-DD 00:00:00'),
+          );
+        } else {
+          tempParams.start_time = '';
+          tempParams.end_time = '';
+        }
+        break;
+      case 'page':
+        tempParams.page = value;
+        handleSearch(convertObject2FormData(tempParams));
+        break;
+      default:
+        break;
+    }
+    console.log('-------------------', tempParams);
+    setQueryParams(tempParams);
+  };
+
+  const handleSearch = (params) => {
+    if (params !== undefined) {
+      getList(params);
+      return;
+    }
+    getList(formData);
+  };
+
+  useEffect(() => {
+    const tempFormData = convertObject2FormData(queryParams);
+    // page变更自动请求接口
+    setFormData(tempFormData);
+  }, [queryParams]);
+
+  return (
+    <PageContent>
+      <Spin spinning={loading}>
+        <div className={styles.searchContent}>
+          <Button
+            className={[styles.marginBottom, styles.marginRight].join(' ')}
+            type="primary"
+            onClick={() => navigate(-1)}
+          >
+            返回
+          </Button>
+          <div className={styles.searchContent}>
+            日期:
+            <RangePicker
+              className={styles.timePicker}
+              onChange={(value) => handleParamsChange('date', value)}
+            />
+            <Button
+              className={styles.marginLeft}
+              type="primary"
+              onClick={() => handleSearch()}
+            >
+              查询
+            </Button>
+          </div>
+        </div>
+        <Table
+          dataSource={data?.list}
+          columns={columns}
+          pagination={data?.pagenation}
+          onChange={({ current }) => handleParamsChange('page', current)}
+        />
+      </Spin>
+    </PageContent>
+  );
+};
+export default HistoryRecord;

+ 229 - 0
src/pages/SmartOps/OperationRecord.js

@@ -0,0 +1,229 @@
+import PageContent from '@/components/PageContent';
+import { getVarValues } from '@/services/DeviceInfo';
+import { useNavigate, useParams, useRequest } from '@umijs/max';
+import { Button, DatePicker, Select, Spin, Table } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+const { Option } = Select;
+
+const OperationRecord = (props) => {
+  const { projectId } = useParams();
+  const navigate = useNavigate();
+
+  const convertObject2FormData = (params) => {
+    const formData = new FormData();
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== null && value !== undefined && value !== NaN) {
+        formData.append(key, value);
+      }
+    });
+    return formData;
+  };
+
+  const defaultParams = {
+    project_id: Number(projectId),
+    s_time: '',
+    e_time: '',
+    cause_type: '',
+    currentPage: 1,
+    pageSize: 10,
+  };
+
+  const [queryParams, setQueryParams] = useState(defaultParams);
+
+  const [formData, setFormData] = useState(
+    convertObject2FormData(defaultParams),
+  );
+
+  const {
+    data,
+    run: getList,
+    loading,
+  } = useRequest((params = formData) => getVarValues(params), {
+    formatResult: (res) => {
+      return res?.data;
+    },
+  });
+
+  const columns = [
+    {
+      title: '操作时间',
+      dataIndex: 'c_time',
+      key: 'c_time',
+      align: 'center',
+      render: (value) => {
+        return <div>{dayjs(value).format('YYYY-MM-DD HH:mm')}</div>;
+      },
+    },
+    {
+      title: '来源',
+      dataIndex: 'cause_type',
+      key: 'cause_type',
+      render: (text) => {
+        if (Number(text) === 0) {
+          return '自主操作';
+        }
+        return '工况建议';
+      },
+    },
+    {
+      title: '点位名称',
+      dataIndex: 'item_alias',
+      key: 'item_alias',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'device_name',
+      key: 'device_name',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+
+    {
+      title: '操作前数值',
+      dataIndex: 'old_value',
+      key: 'old_value',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+
+    {
+      title: '操作后数值',
+      dataIndex: 'new_value',
+      key: 'new_value',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '操作人',
+      dataIndex: 'operator_name',
+      key: 'operator_name',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+  ];
+
+  const handleParamsChange = (key, value) => {
+    const tempParams = {
+      project_id: Number(projectId),
+      s_time: queryParams.s_time || '',
+      e_time: queryParams.e_time || '',
+      cause_type: queryParams.cause_type || '',
+      currentPage: queryParams.currentPage || 1,
+      pageSize: queryParams.pageSize || 10,
+    };
+    debugger;
+    switch (key) {
+      case 'cause_type':
+        tempParams[key] = value;
+        break;
+      case 'date':
+        if (value.length === 2) {
+          tempParams.s_time = dayjs(value[0]).format('YYYY-MM-DD 00:00:00');
+          tempParams.e_time = dayjs(value[1]).format('YYYY-MM-DD 23:59:59');
+        } else {
+          tempParams.s_time = '';
+          tempParams.e_time = '';
+        }
+        break;
+      case 'page':
+        tempParams.currentPage = value;
+        handleSearch(convertObject2FormData(tempParams));
+      default:
+        break;
+    }
+    setQueryParams(tempParams);
+  };
+
+  const handleSearch = (params) => {
+    if (params !== undefined) {
+      getList(params);
+      return;
+    }
+    getList(formData);
+  };
+
+  useEffect(() => {
+    const tempFormData = convertObject2FormData(queryParams);
+    // page变更自动请求接口
+    setFormData(tempFormData);
+  }, [queryParams]);
+
+  return (
+    <PageContent>
+      <Spin spinning={loading}>
+        <div className={styles.searchContent}>
+          <Button
+            className={styles.marginRight}
+            type="primary"
+            onClick={() => navigate(-1)}
+          >
+            返回
+          </Button>
+          日期:
+          <RangePicker
+            className={[styles.timePicker, styles.marginRight].join(' ')}
+            onChange={(value) => handleParamsChange('date', value)}
+          />
+          <span style={{ marginLeft: '20px' }}>来源:</span>
+          <Select
+            placeholder="请选择来源"
+            style={{ width: 200 }}
+            onChange={(value) => handleParamsChange('cause_type', value)}
+            allowClear
+          >
+            <Option value="0">自主操作</Option>
+            <Option value="1">工况建议</Option>
+          </Select>
+          <Button
+            className={styles.marginLeft}
+            type="primary"
+            onClick={() => handleSearch()}
+          >
+            查询
+          </Button>
+        </div>
+
+        <Table
+          dataSource={data?.list || []}
+          columns={columns}
+          pagination={data?.pagination}
+          onChange={({ current }) => {
+            handleParamsChange('page', current);
+          }}
+        />
+      </Spin>
+    </PageContent>
+  );
+};
+export default OperationRecord;

+ 822 - 0
src/pages/SmartOps/WorkAnalysisDetail.js

@@ -0,0 +1,822 @@
+import PageContent from '@/components/PageContent';
+import {
+  queryBackwash,
+  queryBackwashList,
+  queryDesignNob,
+  queryDesignNobList,
+  queryDesignWash,
+  queryDesignWashList,
+  queryDrug,
+  queryDrugList,
+  queryMembrane,
+  queryMembraneList,
+  queryPump,
+  queryPumpList,
+} from '@/services/SmartOps';
+import { UnityAction } from '@/utils/utils';
+import {
+  useNavigate,
+  useParams,
+  useRequest,
+  useSearchParams,
+} from '@umijs/max';
+import { Button, Collapse, DatePicker, Empty, Form, Spin, Tabs } from 'antd';
+import dayjs from 'dayjs';
+import * as echarts from 'echarts';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import styles from './WorkAnalysisDetail.less';
+
+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,
+  },
+  td_pump_nf: {
+    name: '纳滤水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_nf' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_nf' }),
+  },
+  td_pump_uf: {
+    name: '超滤水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_uf' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_uf' }),
+  },
+  td_pump_ro: {
+    // td_pump: {
+    name: '反渗透水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_ro' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_ro' }),
+  },
+  td_pump_mf: {
+    name: '微滤水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_mf' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_mf' }),
+  },
+  td_pump_nf_drug: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_nf_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_nf_drug' }),
+  },
+  td_pump_uf_drug: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_uf_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_uf_drug' }),
+  },
+  td_pump_ro_drug: {
+    // td_pump: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_ro_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_ro_drug' }),
+  },
+  td_pump_mf_drug: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_mf_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_mf_drug' }),
+  },
+};
+
+const { TabPane } = Tabs;
+const { Panel } = Collapse;
+const { RangePicker } = DatePicker;
+
+function WorkAnalysisDetail(props) {
+  const navigate = useNavigate();
+  const { projectId } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const eTime = searchParams.get('eTime');
+
+  const workAnalysis = useMemo(() => {
+    let workAnalysis = {
+      technologys: [],
+      optimizationItems: {},
+      optimizationNumber: 0,
+    };
+    try {
+      workAnalysis = JSON.parse(sessionStorage.workAnalysis);
+      workAnalysis.technologys = workAnalysis.technologys.filter((item) => {
+        if (!TYPE[item]) console.log(item);
+        return TYPE[item];
+      });
+    } catch (error) {
+      console.error(error);
+    }
+    return workAnalysis;
+  }, []);
+  const {
+    technologys,
+    optimizationItems,
+    optimizationNumber,
+    parentName,
+    name,
+  } = workAnalysis;
+
+  const [active, setActive] = useState(technologys[0]);
+  const [current, setCurrent] = useState([]);
+  const [selected, setSelected] = useState('');
+
+  const handleBackClick = () => {
+    UnityAction.sendMsg('ProcessAnalysisDetailBack');
+    navigate(-1);
+  };
+
+  useEffect(() => {
+    UnityAction.addEventListener('SynDev', (deviceCode) =>
+      setCurrent([deviceCode]),
+    );
+    return () => UnityAction.off('SynDev');
+  }, []);
+
+  return (
+    <PageContent>
+      <div className={styles.page}>
+        <Button type="primary" onClick={handleBackClick}>
+          返回
+        </Button>
+        <div className={styles.title}>
+          {parentName}:{name}
+          <span>
+            {optimizationNumber > 0 && `${optimizationNumber}项待优化`}
+          </span>
+        </div>
+        <Tabs
+          activeKey={active}
+          onChange={(key) => {
+            setCurrent([]);
+            setActive(key);
+          }}
+        >
+          {technologys?.map((item) => (
+            <TabPane key={item} tab={TYPE[item]?.name}>
+              <ListContent
+                tab={active}
+                current={active == item ? current : []}
+                setCurrent={setCurrent}
+                selected={selected}
+                setSelected={setSelected}
+                name={name}
+                active={item}
+                optimizationItems={optimizationItems}
+                projectId={projectId}
+                eTime={eTime}
+              />
+            </TabPane>
+          ))}
+        </Tabs>
+      </div>
+    </PageContent>
+  );
+}
+const ListContent = (props) => {
+  const {
+    tab,
+    projectId,
+    active,
+    optimizationItems = {},
+    name,
+    current,
+    setCurrent,
+    eTime,
+    selected,
+    setSelected,
+  } = props;
+
+  const { data, loading, run } = useRequest(
+    () => {
+      let params = {
+        page: 1,
+        page_size: 99999,
+        project_id: projectId,
+      };
+      return TYPE[active]?.device(params);
+    },
+    {
+      formatResult: (res) => {
+        return res.list;
+      },
+    },
+  );
+
+  useEffect(() => {
+    if (tab == active && data) {
+      sendMsg();
+    }
+    UnityAction.on('SynDev', selectedItem);
+    return () => UnityAction.off('SynDev');
+  }, [tab, active, data]);
+
+  const selectedItem = (e) => {
+    setSelected(e);
+    setCurrent([...current, e]);
+  };
+
+  //通知unity切换tab发送tab数据
+  const sendMsg = () => {
+    const SysDevs = {};
+    data?.forEach((item) => {
+      SysDevs[item.device_code] =
+        optimizationItems?.[`${active}:${item.device_code}`] || 0;
+    });
+    const msg = { SysName: name, SysDevs };
+    UnityAction.sendMsg('ProcessAnalysisDetail', JSON.stringify(msg));
+  };
+
+  const activeKeys = useMemo(() => {
+    if (!data) return [];
+    return current.filter((deviceCode) =>
+      data.some((item) => item.device_code == deviceCode),
+    );
+  }, [data, current]);
+
+  const handleClick = (keys) => {
+    // 判断是开启还是关闭
+    // 设置当前选中行
+    if (keys.length > current.length) {
+      const code = keys[keys.length - 1];
+      UnityAction.sendMsg('SynDev', code);
+      setSelected(code);
+    } else if (current.length > 0) {
+      const item = current.find(
+        (item) => keys.findIndex((cur) => cur == item) == -1,
+      );
+      setSelected(item);
+    }
+    setCurrent(keys);
+  };
+
+  const getExtra = (deviceCode) => {
+    if (optimizationItems?.[`${active}:${deviceCode}`]) {
+      return <span style={{ color: '#F5A623' }}>可优化</span>;
+    } else {
+      return <span style={{ color: '#11EEE4' }}>暂无优化</span>;
+    }
+  };
+
+  const getStyle = (code) => {
+    return code == selected ? { backgroundColor: '#1b73c5' } : null;
+  };
+
+  return (
+    <Spin spinning={loading} style={{ minHeight: 300 }}>
+      <Collapse
+        activeKey={current}
+        expandIconPosition="right"
+        onChange={handleClick}
+      >
+        {data?.map((item) => (
+          <Panel
+            style={getStyle(item.device_code)}
+            header={`${item.device_name}(${item.device_code})`}
+            key={item.device_code}
+            extra={getExtra(item.device_code)}
+          >
+            <ChartContent
+              active={active}
+              deviceCode={item.device_code}
+              projectId={projectId}
+              eTime={eTime}
+            />
+          </Panel>
+        ))}
+      </Collapse>
+    </Spin>
+  );
+};
+
+const ChartContent = (props) => {
+  const { deviceCode, projectId, active, eTime } = props;
+  const [visible, setVisible] = useState(false);
+  const [params, setParams] = useState(null);
+  const [time, setTime] = useState([
+    dayjs(eTime).subtract(10, 'minute'),
+    dayjs(eTime),
+  ]);
+  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: deviceCode,
+        page: 1,
+        page_size: 9999,
+        project_id: projectId,
+        ...timerRef.current,
+      };
+      return TYPE[active].chart(params);
+    },
+    {
+      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];
+      const arr = [
+        'ro_wash_interval',
+        'peb_interval',
+        'pac_fr',
+        'ro_nob_interva',
+      ];
+      const valueList = Object.entries(lastItem.optimization)
+        .map(([key, item]) => {
+          if (!arr.includes(key)) return null;
+          return item.remark;
+        })
+        .filter((item) => item);
+      if (valueList.length == 0) return '';
+      return `【${lastItem.c_time}】${valueList.join(',')}`;
+    }
+    return '';
+  }, [data]);
+
+  const searchTime = (type) => {
+    let time = [dayjs().startOf(type), dayjs()];
+    onSearch?.(time);
+  };
+
+  const onSearch = (time) => {
+    console.log(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();
+    };
+  }, []);
+
+  return (
+    <div className={styles.chartBox}>
+      <Form layout="inline" className={styles.form}>
+        <div>
+          <Form.Item label="时间">
+            <RangePicker
+              style={{ width: 430 }}
+              allowClear={false}
+              defaultValue={time}
+              // value={time}
+              onChange={onSearch}
+              format="YYYY-MM-DD HH:mm:ss"
+            ></RangePicker>
+          </Form.Item>
+        </div>
+        <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>
+      </Form>
+      <Spin spinning={loading}>
+        {!data?.list && <Empty style={{ marginBottom: 40 }} />}
+        <div
+          ref={domRef}
+          style={{ height: 300, 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 = '',
+    y2AxisName = '',
+    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 = '投加量';
+      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.std_tmp);
+        // 模拟跨膜压差
+        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;
+    case 'td_pump_nf':
+    case 'td_pump_uf':
+    case 'td_pump_ro':
+    // case 'td_pump':
+    case 'td_pump_mf':
+    case 'td_pump_nf_drug':
+    case 'td_pump_mf_drug':
+    case 'td_pump_mf_drug':
+    case 'td_pump_mf_drug':
+      yAxisName = '频率 Hz';
+      y2AxisName = '电流 A';
+      data?.forEach((item) => {
+        // 实际跨膜压差
+        data1.push(item.frequency);
+        data2.push(item.current);
+        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,
+          yAxisIndex: 1,
+          showSymbol: false,
+        },
+      ];
+      break;
+  }
+  // 过滤失效数据
+  xAxis.forEach((time, index) => {
+    if (time === 'Invalid date') {
+      xAxis[index] = null;
+      data1[index] = null;
+      data2[index] = null;
+      data3[index] = null;
+      data4[index] = null;
+    }
+  });
+  xAxis = xAxis.filter((item) => item);
+  data1 = data1.filter((item) => !isNaN(item));
+  data2 = data2.filter((item) => !isNaN(item));
+  data3 = data3.filter((item) => !isNaN(item));
+  data4 = data4.filter((item) => !isNaN(item));
+
+  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',
+        },
+      },
+      splitNumber: 5,
+      splitLine: {
+        lineStyle: {
+          color: '#fff',
+        },
+      },
+      axisLabel: {
+        color: '#fff',
+      },
+    },
+    series,
+  };
+  if (y2AxisName) {
+    let y1 = option.yAxis;
+    let y2 = JSON.parse(JSON.stringify(y1));
+    y2 = { ...y2, name: y2AxisName };
+    option.yAxis = [y1, y2];
+  }
+  console.log(option);
+  return option;
+}
+export default WorkAnalysisDetail;

+ 53 - 0
src/pages/SmartOps/WorkAnalysisDetail.less

@@ -0,0 +1,53 @@
+.title {
+  background-color: #6eb3f3;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 28px;
+  height: 66px;
+  color: #213b58;
+  margin: 1% 0;
+  padding: 0 12px;
+}
+
+.form {
+  margin-bottom: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.page {
+  :global {
+    .ant-collapse > .ant-collapse-item {
+      border: 1px solid #329bfe;
+      background: #2466a4;
+      padding: 0 15px;
+      margin-bottom: 30px;
+      border-radius: 5px;
+      &.ant-collapse-item-active > .ant-collapse-header {
+        border-bottom: 1px solid #7a9dcd;
+      }
+      > .ant-collapse-header {
+        background: transparent;
+        color: #fff;
+        height: 62px;
+        font-size: 22px;
+        padding: 0;
+        border-bottom: 1px solid transparent;
+        .ant-collapse-arrow {
+          font-size: 22px;
+          right: 0;
+        }
+        .ant-collapse-extra {
+          position: absolute;
+          right: 30px;
+        }
+      }
+    }
+    .ant-collapse-content {
+      padding: 15px 0;
+      border-top: none;
+    }
+  }
+}

+ 61 - 0
src/pages/SmartOps/components/BarModal.js

@@ -0,0 +1,61 @@
+import DateRadio from '@/components/DateRadio';
+import BarChartModule from '@/components/ManagementPage/BarChartModule';
+import ChartModule from '@/components/ManagementPage/chartModule';
+import { Modal } from 'antd';
+import { useState } from 'react';
+
+const BarModal = ({ type = 1, title, data, visible, onCancel, onChange }) => {
+  const [tab, setTab] = useState('1');
+
+  const option = {
+    yName: '水量(t)',
+    xData: ['00:00', '01:15', '02:30', '03:45', '05:00', '06:15', '07:30'],
+    dataList: [
+      {
+        type: 0,
+        name: '进水水量',
+        data: [820, 932, 901, 934, 1290, 1330, 1320],
+      },
+      {
+        type: 0,
+        name: '进水水量111',
+        data: [420, 932, 601, 934, 1990, 1330, 1520],
+      },
+      {
+        type: 0,
+        name: '预测出水量',
+        data: [720, 832, 601, 834, 1190, 1230, 1220],
+      },
+      {
+        type: 0,
+        name: '实际出水量',
+        data: [820, 632, 501, 534, 1190, 1130, 1120],
+      },
+    ],
+  };
+
+  return (
+    <Modal
+      title={title}
+      visible={visible}
+      onCancel={onCancel}
+      footer={null}
+      width={600}
+    >
+      <DateRadio onChange={(tab) => onChange(tab)} />
+
+      <div style={{ height: '600px' }}>
+        {type == 1 && (
+          <BarChartModule
+            xData={['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}
+            dataList={[
+              { name: 'xzhou ', data: [120, 200, 150, 80, 70, 110, 130] },
+            ]}
+          />
+        )}
+        {type == 2 && <ChartModule {...option} />}
+      </div>
+    </Modal>
+  );
+};
+export default BarModal;

+ 28 - 0
src/pages/SmartOps/components/BarModal.less

@@ -0,0 +1,28 @@
+.top {
+  margin-bottom: 10px;
+  display: flex;
+  justify-content: right;
+  .tabContent {
+    width: 180px;
+    display: flex;
+    background: #061134;
+    color: #4a90e2;
+    border: 1px solid #4a90e2;
+    .tab {
+      flex-wrap: 0;
+      text-align: center;
+      line-height: 50px;
+      width: 60px;
+      height: 50px;
+      border-right: 1px solid #4a90e2;
+    }
+    .tabActive {
+      .tab;
+      background: #4a90e2;
+      color: #fff;
+    }
+    .tab:last-child {
+      border-right: none;
+    }
+  }
+}

+ 52 - 0
src/pages/SmartOps/components/ThresholdBar.js

@@ -0,0 +1,52 @@
+import React, { useMemo, useRef } from 'react';
+import { Popover } from 'antd';
+import styles from './ThresholdBar.less';
+
+const ThresholdBar = props => {
+  const { current, thresholds = [] } = props;
+  const boxRef = useRef();
+
+  const { min, max } = useMemo(() => {
+    let myThresholds = [...thresholds];
+    if (thresholds.length == 1) {
+      if (thresholds[0] > current) {
+        return { min: '', max: thresholds[0] };
+      } else {
+        return { min: thresholds[0], max: '' };
+      }
+    }
+    myThresholds.sort((a, b) => a - b);
+    let index = myThresholds.findIndex(item => item > current);
+    return { min: myThresholds[index - 1], max: myThresholds[index] };
+  }, [current, thresholds]);
+
+  const getLeft = () => {
+    if (thresholds.length == 1) {
+      if (thresholds[0] > current) {
+        return 0;
+      } else {
+        return '100%';
+      }
+    } else {
+      let number = (current - min) / (max - min);
+      return number * 100 + '%';
+    }
+  };
+
+  return (
+    <div className={styles.bar}>
+      <div className={styles.box} ref={boxRef}>
+        <div className={`${styles.scale} ${styles.min}`}>{min}</div>
+        <div className={`${styles.scale} ${styles.max}`}>{max}</div>
+        <div className={`${styles.scale} ${styles.top}`} style={{ left: getLeft() }}>
+          {parseFloat(current).toFixed(2)}
+        </div>
+        <div className={styles.current} style={{ left: getLeft() }}>
+          <div className={styles.currentBar}></div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ThresholdBar;

+ 46 - 0
src/pages/SmartOps/components/ThresholdBar.less

@@ -0,0 +1,46 @@
+.bar {
+  padding: 30px 14px;
+}
+
+.box {
+  width: 100%;
+  height: 6px;
+  position: relative;
+  background-color: #D45C41;
+
+}
+.scale {
+  position: absolute;
+  top: 16px;
+  line-height: 1.2;
+  font-size: 16px;
+  word-break: keep-all;
+  transform: translateX(-50%);
+
+  &.top {
+    top: inherit;
+    bottom: 16px;
+  }
+  &.min {
+    left: 0;
+  }
+  &.max {
+    left: 100%;
+  }
+}
+.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;
+  // }
+}

+ 142 - 0
src/pages/SmartOps/components/VideoAnalysis.js

@@ -0,0 +1,142 @@
+import { UnityAction } from '@/utils/utils';
+import { Collapse, Empty, Spin, Tabs } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './VideoAnalysis.less';
+
+const { TabPane } = Tabs;
+const { Panel } = Collapse;
+
+function VideoAnalysis(props) {
+  const { data, loading } = props;
+
+  const [prevKey, setPrevKey] = useState();
+  const [selectedName, setSelectedName] = useState('');
+
+  const handleCollapse = (key) => {
+    if (key === prevKey) return;
+    setPrevKey(key);
+    const device = data.list.find((item) => item.id === key)?.device_name;
+    if (device) {
+      UnityAction.sendMsg('SynCam', device);
+    }
+  };
+
+  useEffect(() => {
+    UnityAction.on('SynCam', selectedItem);
+    return () => UnityAction.off('SynCam', selectedItem);
+  }, [data?.list]);
+
+  //滚动到相应位置
+  const selectedItem = (e) => {
+    setSelectedName(e);
+    const dom = document.querySelector(`div[data-name="${e}"]`);
+    if (dom) {
+      let v = document.getElementById('videoContent');
+      v.scrollTop = dom.offsetTop;
+    }
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <div
+        id="videoContent"
+        className={styles.page}
+        style={{ height: 'calc(100vh - 422px)', overflow: 'auto' }}
+      >
+        {data?.list?.length > 0 ? (
+          data?.list?.map((item) => (
+            <div data-name={item.device_name}>
+              <Collapse
+                defaultActiveKey={[item.id]}
+                key={item.id}
+                className={
+                  selectedName == item.device_name || prevKey == item.id
+                    ? styles.tableSelect
+                    : styles.table
+                }
+                expandIconPosition="right"
+                onChange={(e) => {
+                  handleCollapse(item.id);
+                }}
+              >
+                <Panel header={item.device_name} key={item.id}>
+                  <AnalysisContent item={item} />
+                </Panel>
+              </Collapse>
+            </div>
+          ))
+        ) : (
+          <Empty />
+        )}
+        {/* {data?.list?.map(item => (
+          <Collapse
+            defaultActiveKey={[item.id]}
+            key={item.id}
+            className={
+              selectedName == item.device_name || prevKey == item.id
+                ? styles.tableSelect
+                : styles.table
+            }
+            expandIconPosition="right"
+            onChange={e => {
+              handleCollapse(item.id);
+            }}
+          >
+            <Panel header={item.device_name} key={item.id}>
+              <AnalysisContent item={item} />
+            </Panel>
+          </Collapse>
+        ))} */}
+      </div>
+    </Spin>
+  );
+}
+
+function AnalysisContent({ item }) {
+  const handleImgClick = () => {
+    localStorage.setItem('preview', JSON.stringify(item.data));
+    UnityAction.sendMsg('SensorPic');
+  };
+
+  return (
+    <div className={styles.box}>
+      {/* <div className={styles.item} style={{ width: '48%' }}>
+        <div className={styles.label} style={{ lineHeight: '66px' }}>
+          温度数据:
+        </div>
+        <div className={styles.content}>
+          <NewNumberBar breakdown={breakdown} exception={exception} current={0.05} scale />
+        </div>
+      </div>
+      <div className={styles.item} style={{ width: '48%' }}>
+        <div className={styles.label} style={{ lineHeight: '66px' }}>
+          噪音数据:
+        </div>
+        <div className={styles.content}>
+          <NewNumberBar breakdown={breakdown} exception={exception} current={0.05} scale />
+        </div>
+      </div> */}
+      <div className={styles.item}>
+        <div className={styles.label}>画面报警:</div>
+        <div className={styles.content}>
+          {item.event_type}
+          <img className={styles.img} src={item.url} onClick={handleImgClick} />
+        </div>
+      </div>
+      {/* <div className={styles.item}>
+        <div className={styles.label}>可能原因:</div>
+        <div className={styles.content}>
+          建议调整杀菌周期为27分,下调10.00%建议调整杀菌周期为27分,下调10.00%建议调整杀菌周期为27分,下调10.00%建议调整杀菌周期为27分,下调10.00%
+        </div>
+      </div>
+      <div className={styles.item}>
+        <div className={styles.label}>解决方案:</div>
+        <div className={styles.content}>
+          建议调整杀菌周期为27分,下调10.00%建议调整杀菌周期为27分,下调10.00%建议调整杀菌周期为27分,下调10.00%建议调整杀菌周期为27分,下调10.00%
+        </div>
+      </div> */}
+    </div>
+  );
+}
+
+export default VideoAnalysis;

+ 72 - 0
src/pages/SmartOps/components/VideoAnalysis.less

@@ -0,0 +1,72 @@
+.box {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  .item {
+    width: 100%;
+    margin-bottom: 15px;
+    display: flex;
+    align-items: flex-start;
+    font-size: 22px;
+    line-height: 28px;
+    color: #fff;
+    .label {
+      width: 114px;
+      flex-shrink: 0;
+    }
+    .content {
+      flex: 1;
+    }
+  }
+}
+
+.img {
+  width: 120px;
+  height: 90px;
+  display: block;
+  margin-top: 10px;
+}
+.page {
+  .tableSelect {
+    background-color: #2466a4 !important;
+  }
+  .table {
+    background-color: #064779;
+  }
+
+  :global {
+    .ant-collapse > .ant-collapse-item {
+      border: 1px solid #329bfe;
+      // background: #2466a4;
+      padding: 0 15px;
+      margin-bottom: 15px;
+      border-radius: 5px;
+      &.ant-collapse-item-active > .ant-collapse-header {
+        border-bottom: 1px solid #7a9dcd;
+      }
+      > .ant-collapse-header {
+        background: none;
+        color: #fff;
+        height: 62px;
+        font-size: 22px;
+        padding: 0;
+        border-bottom: 1px solid transparent;
+        .ant-collapse-arrow {
+          font-size: 22px;
+          right: 0;
+        }
+        .ant-collapse-extra {
+          position: absolute;
+          right: 30px;
+        }
+      }
+    }
+    .ant-collapse:last-child > .ant-collapse-item {
+      margin-bottom: 0;
+    }
+    .ant-collapse-content {
+      padding: 15px 0;
+      border-top: none;
+    }
+  }
+}

+ 50 - 0
src/pages/SmartOps/components/WorkAnalysis.js

@@ -0,0 +1,50 @@
+import { RightOutlined } from '@ant-design/icons';
+import { useNavigate } from '@umijs/max';
+import { Spin } from 'antd';
+import styles from './WorkAnalysis.less';
+
+function WorkAnalysis(props) {
+  const navigate = useNavigate();
+  const { projectId, workAnalysisRequest: data, loading, eTime } = props;
+
+  const project_categorys = data?.project_categorys || [];
+  const toDetail = (item) => {
+    sessionStorage.workAnalysis = JSON.stringify(item);
+    navigate(
+      `/smart-ops/work-analysis-detail/${projectId}?typeList=${item.technologys.join(
+        ',',
+      )}&eTime=${eTime}`,
+    );
+  };
+  return (
+    <Spin spinning={loading}>
+      <div style={{ height: 'calc(100vh - 422px)', overflow: 'auto' }}>
+        {project_categorys?.map((item) => (
+          <div className={styles.box} key={item.type}>
+            <div className={styles.title}>{item.name}</div>
+            <ul className={styles.list}>
+              {item.childs?.map((cItem) => (
+                <li key={cItem.type}>
+                  <div className={styles.listTitle}>{cItem.name}</div>
+                  <div className={styles.btn} onClick={() => toDetail(cItem)}>
+                    {cItem.optimizationNumber == 0 ? (
+                      <span style={{ color: '#11EEE4' }}>暂无优化</span>
+                    ) : (
+                      <span style={{ color: '#F5A623' }}>
+                        可优化({cItem.optimizationNumber})
+                      </span>
+                    )}
+                    <RightOutlined className={styles.icon} />
+                    {/* <Icon className={styles.icon} type="right" /> */}
+                  </div>
+                </li>
+              ))}
+            </ul>
+          </div>
+        ))}
+      </div>
+    </Spin>
+  );
+}
+
+export default WorkAnalysis;

+ 42 - 0
src/pages/SmartOps/components/WorkAnalysis.less

@@ -0,0 +1,42 @@
+.box {
+  background: #064779;
+  padding: 22px;
+  border-radius: 5px;
+  padding-bottom: 5px;
+  margin-bottom: 16px;
+}
+.title {
+  font-size: 28px;
+  color: #329bfe;
+  line-height: 38px;
+}
+.list {
+  margin: 0;
+  padding: 0;
+  padding-left: 10px;
+  li {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 24px;
+    border-bottom: 1px solid #2a6fa3;
+    padding: 15px 0;
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+  .listTitle {
+    color: #fff;
+  }
+  .btn {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    line-height: 1;
+  }
+  .icon {
+    color: #fff;
+    margin-left: 20px;
+    font-size: 24px;
+  }
+}

+ 375 - 0
src/pages/SmartOps/index.js

@@ -0,0 +1,375 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import {
+  getHistoryRecord,
+  queryProjectConfig,
+  querySimulationProfit,
+} from '@/services/SmartOps';
+import { getCameraList, getDetail } from '@/services/dumu';
+import { GetTokenFromUrl, UnityAction } from '@/utils/utils';
+import {
+  connect,
+  useNavigate,
+  useParams,
+  useRequest,
+  useSearchParams,
+} from '@umijs/max';
+import { Button, Tabs } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import Analysis from './Analysis';
+import VideoAnalysis from './components/VideoAnalysis';
+import WorkAnalysis from './components/WorkAnalysis';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+const icon06 = require('@/assets/smartOps/icon06.png');
+const icon07 = require('@/assets/smartOps/icon07.png');
+
+let timer = '';
+
+function SmartOps(props) {
+  const { list, dispatch, loading } = props;
+
+  const navigate = useNavigate();
+  const { projectId } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const time = searchParams.get('time');
+  const idList = searchParams.get('idList');
+  console.log('-------------------', time, idList);
+
+  // const [eTime, setETime] = useState(time ? time : dayjs().format('YYYY-MM-DD HH:mm:ss'));
+  // const [sTime, setSTime] = useState(
+  //   time
+  //     ? dayjs(time)
+  //         .subtract(10, 'minute')
+  //         .format('YYYY-MM-DD HH:mm:ss')
+  //     : s_time
+  // );
+  // if (ruleList?.indexOf(projectId * 1) != -1) {
+  //   isNewRole = true;
+  // }
+
+  // const showTime = useMemo(() => {
+  //   return dayjs(eTime).format('MM-DD HH:mm');
+  // }, [eTime]);
+
+  const [tableData, setTableData] = useState([]);
+  const signalRef = useRef();
+
+  const {
+    data: reportData,
+    loading: reportLoading,
+    run: getReportData,
+  } = useRequest(
+    () =>
+      getHistoryRecord(
+        convertObject2FormData({ project_id: Number(projectId) }),
+      ),
+    {
+      manual: true,
+      formatResult: (res) => {
+        let data = res.data.list[0];
+        data.Num2Length = data.Num2.split(',').length;
+        return data;
+      },
+    },
+  );
+
+  const [sTime, eTime] = useMemo(() => {
+    let sTime = null,
+      eTime = null;
+    if (time) {
+      eTime = time;
+      sTime = dayjs(eTime).subtract(10, 'minute').format('YYYY-MM-DD HH:mm:ss');
+    } else if (reportData) {
+      eTime = reportData.CTime;
+      sTime = dayjs(eTime).subtract(10, 'minute').format('YYYY-MM-DD HH:mm:ss');
+    }
+    return [sTime, eTime];
+  }, [reportData]);
+
+  const initDate = () => {
+    //工艺分析
+    dispatch({
+      type: 'smartOps/queryList',
+      payload: {
+        project_id: projectId * 1,
+        projectId: projectId * 1,
+        ids: idList || reportData.Num2,
+        page_size: 999,
+        isNewRole: [46, 65, 92, 94].includes(projectId * 1),
+      },
+      callback: (data) => {
+        UnityAction.sendMsg('ProcessAnalysis', JSON.stringify(data?.list));
+      },
+    });
+  };
+
+  const handlerHistoryClick = () => {
+    navigate(
+      `/smart-ops/history-record/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+    );
+  };
+  const handlerRecordClick = () => {
+    navigate(
+      `/smart-ops/operation-record/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+    );
+  };
+
+  // 工况分析
+  const {
+    data: workAnalysisRequest,
+    run: runWork,
+    loading: loadingWork,
+  } = useRequest(
+    () =>
+      queryProjectConfig({
+        project_id: projectId,
+        s_time: sTime,
+        e_time: eTime,
+      }),
+    {
+      manual: true,
+      onSuccess(res) {
+        if (!res) return;
+        UnityAction.sendMsg(
+          'WorkAnalysis',
+          JSON.stringify(res.project_categorys),
+        );
+      },
+    },
+  );
+
+  const optimizationNumber = workAnalysisRequest?.optimizationNumber || 0;
+
+  //感知分析
+  const { run: detailRun } = useRequest(getDetail, {
+    manual: true,
+    fetchKey: (id) => id,
+    cacheKey: (id) => id,
+    onSuccess: (data) => {
+      var item = tableData?.list?.find((child) => child.id === data.id);
+      if (item) {
+        item.url = base64ToImageUrl(data.event_bg);
+        item.data = data;
+      }
+      var data = {
+        list: [...tableData?.list],
+        pagination: tableData?.pagination,
+      };
+      setTableData(data);
+    },
+  });
+  const { run, loading: loadingVideo } = useRequest(
+    () =>
+      getCameraList(projectId, {
+        s_time: sTime,
+        e_time: eTime,
+        pageSize: 999,
+      }),
+    {
+      manual: true,
+      onSuccess: (data) => {
+        setTableData(data);
+        sendUnityMsg(data?.list);
+        // data?.list?.forEach(item => {
+        //   detailRun(item.id, { signal: signalRef.current.signal });
+        // });
+      },
+    },
+  );
+
+  const videoAnalysisNumber = tableData?.pagination?.total || 0;
+  const AnalysisNumber = list?.pagenation?.total || 0;
+
+  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;
+  }
+  // 利润
+  const { data: profitData, run: getProfit } = useRequest(
+    () =>
+      querySimulationProfit({
+        project_id: projectId,
+        s_time: dayjs(eTime).subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'),
+        e_time: eTime,
+      }),
+    {
+      formatResult(data) {
+        if (!data?.info) return '-';
+
+        return Object.values(data.info).reduce(
+          (total, currentValue) => total + currentValue,
+          0,
+        );
+      },
+      manual: true,
+    },
+  );
+
+  const showTime = useMemo(() => {
+    if (!eTime) return '';
+    return dayjs(eTime).format('MM-DD HH:mm');
+  }, [eTime]);
+
+  const sendUnityMsg = (list) => {
+    if (!list || list?.length == 0) return;
+    const names = list.map((item) => item.device_name);
+    UnityAction.sendMsg('SensorAnalysis', names.join(','));
+  };
+
+  const onChangeTab = (type) => {
+    UnityAction.sendMsg('SmartAnalysisTab', type);
+    if (type == '3') {
+      tableData?.list?.forEach((item) => {
+        detailRun(item.id, { signal: signalRef.current.signal });
+      });
+    }
+  };
+
+  useEffect(() => {
+    if (!eTime) return;
+    initDate();
+    runWork();
+    run();
+    getProfit();
+  }, [eTime]);
+
+  useEffect(() => {
+    if (!time) {
+      getReportData();
+      console.log('--------10分钟刷新数据--------', eTime);
+      timer = setInterval(() => {
+        getReportData();
+      }, 60000);
+    }
+
+    dispatch({
+      type: 'smartOps/queryProcessSection',
+      payload: projectId,
+    });
+
+    // 通知unity当前处于工况分析
+    UnityAction.sendMsg('SmartAnalysisTab', 1);
+
+    const controller = new AbortController();
+    signalRef.current = controller;
+
+    return () => {
+      signalRef.current.abort();
+      clearInterval(timer);
+    };
+  }, []);
+
+  return (
+    <PageContent>
+      <PageTitle>智慧分析</PageTitle>
+      {time && (
+        <Button
+          type="primary"
+          style={{ marginBottom: 20 }}
+          onClick={() => navigate(-1)}
+        >
+          返回
+        </Button>
+      )}
+      <div className={`card-box ${styles.topContent}`}>
+        <div className={styles.titleContent}>
+          <span className={styles.time}>{showTime}</span>
+          {!time && (
+            <div style={{ display: 'flex' }}>
+              <div className={styles.iconLeft} onClick={handlerHistoryClick} />
+              <div className={styles.iconRight} onClick={handlerRecordClick} />
+            </div>
+          )}
+        </div>
+        <div className={styles.middle}>
+          <div className={styles.left}>
+            <div className={styles.in} />
+            <div className={styles.out} />
+          </div>
+          <div className={styles.right}>
+            <div className={styles.item1}>
+              工况分析:
+              {optimizationNumber > 0
+                ? `${optimizationNumber}项可优化`
+                : '暂无优化'}
+            </div>
+            <div className={styles.item2}>
+              工艺分析:
+              {list?.pagenation?.total > 0
+                ? `${list?.pagenation?.total}项可优化`
+                : '暂无优化'}
+            </div>
+            <div className={styles.item3}>
+              感知分析:
+              {videoAnalysisNumber > 0
+                ? `${videoAnalysisNumber}项可优化`
+                : '暂无优化'}
+            </div>
+          </div>
+        </div>
+        <div className={styles.text}>通过智慧分析预计可省{profitData}元</div>
+      </div>
+      <div className={styles.tabContent}>
+        <Tabs
+          defaultActiveKey="1"
+          items={[
+            {
+              label: `工况分析(${loadingWork ? '-' : optimizationNumber})`,
+              key: '1',
+              children: (
+                <WorkAnalysis
+                  workAnalysisRequest={workAnalysisRequest}
+                  projectId={projectId}
+                  loading={loadingWork}
+                  eTime={eTime}
+                />
+              ),
+            },
+            {
+              label: `工艺分析(${loading ? '-' : AnalysisNumber})`,
+              key: '2',
+              children: <Analysis />,
+            },
+            {
+              label: `感知分析(${loadingVideo ? '-' : videoAnalysisNumber})`,
+              key: '3',
+              children: (
+                <VideoAnalysis data={tableData} loading={loadingVideo} />
+              ),
+            },
+          ]}
+          onChange={onChangeTab}
+        ></Tabs>
+      </div>
+    </PageContent>
+  );
+}
+
+const convertObject2FormData = (params) => {
+  const formData = new FormData();
+  Object.entries(params).forEach(([key, value]) => {
+    if (value !== null && value !== undefined && value !== NaN) {
+      formData.append(key, value);
+    }
+  });
+  return formData;
+};
+
+export default connect(({ smartOps, loading }) => ({
+  list: smartOps.list,
+  loading: loading.models.smartOps,
+  processList: smartOps.processList,
+}))(SmartOps);

+ 173 - 0
src/pages/SmartOps/index.less

@@ -0,0 +1,173 @@
+.topContent {
+  padding: 8px 12px;
+  margin: 16px 0;
+  color: #f8f8f8;
+  .titleContent {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .time {
+      font-size: 20px;
+      color: #6e6e6e;
+    }
+    .iconLeft {
+      width: 27px;
+      height: 27px;
+      background: url('@/assets/smartOps/icon06.png') no-repeat center;
+      background-size: 100% 100%;
+    }
+    .iconRight {
+      .iconLeft;
+      margin-left: 20px;
+      background: url('@/assets/smartOps/icon07.png') no-repeat center;
+      background-size: 100% 100%;
+    }
+  }
+  .middle {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .left {
+      position: relative;
+      .in {
+        width: 212px;
+        height: 212px;
+        background: url('@/assets/smartOps/icon04.png') no-repeat center;
+        background-size: 100% 100%;
+        animation-name: scaleUp;
+        animation-duration: 5s;
+        animation-iteration-count: infinite;
+        animation-timing-function: linear;
+      }
+      .out {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 212px;
+        height: 212px;
+        background: url('@/assets/smartOps/icon05.png') no-repeat center;
+        background-size: 100% 100%;
+        animation-name: spin;
+        animation-duration: 30s;
+        animation-iteration-count: infinite;
+        animation-timing-function: linear;
+      }
+    }
+    .right {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-end;
+      .item1 {
+        width: 475px;
+        height: 60px;
+        padding-left: 64px;
+        white-space: nowrap;
+        line-height: 60px;
+        margin-bottom: 6px;
+        background: url('@/assets/smartOps/icon01.png') no-repeat center;
+        background-size: 100% 100%;
+      }
+      .item2 {
+        width: 427px;
+        height: 60px;
+        padding-left: 24px;
+        white-space: nowrap;
+        line-height: 60px;
+        margin-bottom: 6px;
+        background: url('@/assets/smartOps/icon02.png') no-repeat center;
+        background-size: 100% 100%;
+      }
+      .item3 {
+        background: url('@/assets/smartOps/icon03.png') no-repeat center;
+        background-size: 100% 100%;
+        width: 471px;
+        height: 60px;
+        padding-left: 64px;
+        white-space: nowrap;
+        line-height: 60px;
+      }
+    }
+  }
+  .text {
+    padding: 8px 54px;
+    line-height: 24px;
+    text-align: center;
+  }
+}
+
+.anaBtn {
+  font-size: 12px !important;
+  padding: 6px;
+  line-height: 12px;
+  height: auto !important;
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+
+  to {
+    transform: rotate(360deg);
+  }
+}
+@keyframes scaleUp {
+  0% {
+    transform: translate(0px, 0px);
+  }
+  25% {
+    transform: translate(0px, -5px);
+  }
+  75% {
+    transform: translate(0px, 5px);
+  }
+  100% {
+    transform: translate(0px, 0px);
+  }
+}
+
+.marginBottom {
+  margin-bottom: 20px;
+}
+.marginLeft {
+  margin-left: 20px;
+}
+.marginRight {
+  margin-right: 20px;
+}
+.searchContent {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  color: #ffffff;
+  font-size: 18px;
+  white-space: nowrap;
+  :global {
+    .ant-select {
+      margin: 0 40px 0 10px;
+      background-color: #284e83;
+    }
+  }
+}
+.timePicker {
+  background-color: #284e83;
+}
+
+.form {
+  width: 100%;
+  margin-bottom: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.tableSelect {
+  background-color: #2466a4;
+}
+.tabContent {
+  :global {
+    .ant-table-tbody > tr:nth-child(2n):not(.ant-table-expanded-row) {
+      background-image: url('~@/assets/tbodyBg1.png');
+    }
+  }
+}

+ 88 - 0
src/pages/SmartOps/models/smartOps.js

@@ -0,0 +1,88 @@
+import {
+  queryAnalysisDict,
+  queryProcessSection,
+} from '@/services/OperationManagement';
+import { queryList, queryListNew } from '@/services/diagnosticTec';
+export default {
+  namespace: 'smartOps',
+
+  state: {
+    list: [],
+    processList: [],
+  },
+
+  effects: {
+    *queryList({ payload, callback }, { call, put, select }) {
+      if (!payload.ids) {
+        yield put({
+          type: 'save',
+          payload: {
+            list: {
+              list: [],
+              pagenation: { total: 0 },
+            },
+          },
+        });
+        return;
+      }
+      if (payload.isNewRole) {
+        const analysisDict = yield call(queryAnalysisDict, { pageSize: 9999 });
+        const response = yield call(queryListNew, payload);
+        if (response) {
+          response?.data?.list?.map((item) => {
+            console.log('--------------------------', analysisDict);
+            var reason = analysisDict?.data?.find(
+              (reason) => reason.id == item.Reason,
+            );
+            if (reason) {
+              item.Reason = reason.content;
+            }
+            var FixPlanArr = item.FixPlan.split(',');
+            if (FixPlanArr.length > 0) {
+              item.FixPlan = [];
+              FixPlanArr.map((fixItem) => {
+                var fixPlan = analysisDict?.data?.find(
+                  (reason) => reason.id == fixItem,
+                );
+                if (fixPlan) item.FixPlan.push(fixPlan);
+              });
+              console.log(item.FixPlan);
+            }
+          });
+          yield put({
+            type: 'save',
+            payload: { list: response.data },
+          });
+          callback && callback?.(response.data);
+        }
+      } else {
+        const response = yield call(queryList, payload);
+        if (response) {
+          yield put({
+            type: 'save',
+            payload: { list: response.data },
+          });
+        }
+        callback && callback?.(response.data);
+      }
+    },
+    *queryProcessSection({ payload }, { call, put }) {
+      const list = yield call(queryProcessSection, payload);
+      if (list) {
+        yield put({
+          type: 'save',
+          payload: { processList: list.data },
+        });
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 49 - 0
src/services/DeviceInfo.js

@@ -0,0 +1,49 @@
+import { stringify } from 'qs';
+import { request } from 'umi';
+
+export async function downloadPatrolQRCodeFile(params) {
+  return request(`/device/item-path`, {
+    method: 'POST',
+    data: params,
+  });
+}
+export async function getDeviceInfoByPath(params) {
+  return request(`/device/item-path`, {
+    method: 'POST',
+    data: params,
+  });
+}
+export async function getModelAttributes(params) {
+  return request(
+    `/device-info/${params.projectId}/${params.deviceCode}?${stringify(
+      params,
+    )}`,
+  );
+}
+export async function getDeviceDataItemDictImpl(params) {
+  return request(`/config/device-realtime-data/list/${params.projectId}`);
+}
+export async function getRealtimeData(params) {
+  return request(`/jinke-cloud/device/current-data`, {
+    method: 'POST',
+    data: params,
+  });
+}
+export async function getFolderTransfer(params) {
+  return request(`/file-service/${params.projectId}/folders/transfer`);
+}
+
+export async function DeviceRecentChart(params) {
+  return request(
+    `/device-info-chart/${params.projectId}/${params.deviceCode}?${stringify(
+      params,
+    )}`,
+  );
+}
+export async function getVarValues(params) {
+  return request(`/api/v1/scada/get-var-values`, {
+    method: 'POST',
+    data: params,
+    headers: { ContentType: 'application/x-www-form-urlencoded' },
+  });
+}

+ 3 - 3
src/services/OperationManagement.js

@@ -306,7 +306,7 @@ export async function queryQualityAbnormal(params) {
   const res = await request(`/api/task/v1/water-monitor/list`, {
     method: 'POST',
     headers: { ContentType: 'application/json' },
-    body: params,
+    data: params,
   });
   return res;
 }
@@ -315,7 +315,7 @@ export async function dealQualityAbnormal(params) {
   await request(`/api/task/v1/set/water-monitor`, {
     method: 'POST',
     headers: { ContentType: 'application/json' },
-    body: params,
+    data: params,
   });
 }
 
@@ -393,4 +393,4 @@ export async function getChemicalAgents(projectID) {
 export async function getScadaPage(params = {}) {
   const res = await request(`/api/v1/scada/page?${stringify(params)}`);
   return res?.data;
-}
+}

+ 59 - 20
src/services/SmartOps.js

@@ -1,4 +1,3 @@
-import dayjs from 'dayjs';
 import { stringify } from 'qs';
 import { request } from 'umi';
 
@@ -280,14 +279,14 @@ export async function querySimulationProfit(data) {
   let res = await request(
     `/api/simulations/v1/profit/summary?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 export async function conditionEstimate(params) {
   let res = await request(`/api/energy/v1/condition/estimate`, {
     method: 'POST',
     data: params,
   });
-  return res;
+  return res?.data;
 }
 
 // 项目大水量冲洗设计:拉取设计列表
@@ -295,13 +294,13 @@ export async function queryDesignWashList(data) {
   let res = await request(
     `/api/simulations/v1/design/wash/list?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 
 // 运行记录:拉取大水量冲洗
 export async function queryDesignWash(data) {
   let res = await request(`/api/simulations/v1/record/wash?${stringify(data)}`);
-  return res;
+  return res?.data;
 }
 
 // 项目拉取非氧化杀菌列表
@@ -309,13 +308,13 @@ export async function queryDesignNobList(data) {
   let res = await request(
     `/api/simulations/v1/design/nob/list?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 
 // 运行记录:拉取非氧化杀菌运行记录
 export async function queryDesignNob(data) {
   let res = await request(`/api/simulations/v1/record/nob?${stringify(data)}`);
-  return res;
+  return res?.data;
 }
 
 // 项目泵设计:拉取设计列表  type膜类型 mf | uf | nf | ro
@@ -323,13 +322,13 @@ export async function queryPumpList(data) {
   let res = await request(
     `/api/simulations/v1/design/pump/list?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 
 // 运行记录:拉取泵运行记录  type膜类型 mf | uf | nf | ro
 export async function queryPump(data) {
   let res = await request(`/api/simulations/v1/record/pump?${stringify(data)}`);
-  return res;
+  return res?.data;
 }
 
 // 水厂工况
@@ -364,13 +363,32 @@ export async function queryRealEstimateChart(project_id) {
 }
 
 // 项目配置:获取所有拥有配置的项目
-export async function queryProjectConfig(projectId) {
-  let res = await request(
-    `/api/simulations/v1/project?project_id=${projectId}&s_time=${dayjs().format(
-      'YYYY-MM-DD HH:mm:ss',
-    )}&e_time=${dayjs().format('YYYY-MM-DD HH:mm:ss')}`,
-  );
-  return { data: res.data.info.technologys };
+export async function queryProjectConfig(params) {
+  let res = await request(`/api/simulations/v1/project?${stringify(params)}`);
+  let project_categorys = res.data.info.project_categorys;
+  // 全厂优化数
+  let optimizationNumber = 0;
+  project_categorys.forEach((item) => {
+    // 工艺单元优化数
+    item.optimizationNumber = 0;
+    item.childs.forEach((cItem) => {
+      // 工艺系统优化数
+      cItem.optimizationNumber = 0;
+      Object.entries(cItem.optimizationItems || {}).map(
+        ([deviceCode, count]) => {
+          if (count > 0) {
+            cItem.optimizationNumber++;
+          }
+        },
+      );
+      cItem.parentName = item.name;
+      // 记录工艺段的优化数量
+      item.optimizationNumber += cItem.optimizationNumber;
+    });
+    // 记录总的优化数量
+    optimizationNumber += item.optimizationNumber;
+  });
+  return { data: { project_categorys, optimizationNumber } };
 }
 
 // 项目膜设计:拉取设计列表  type膜类型 mf | uf | nf | ro
@@ -378,7 +396,7 @@ export async function queryMembraneList(data) {
   let res = await request(
     `/api/simulations/v1/design/membrane/list?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 
 // 运行记录:拉取非氧化杀菌运行记录  type膜类型 mf | uf | nf | ro
@@ -386,7 +404,7 @@ export async function queryMembrane(data) {
   let res = await request(
     `/api/simulations/v1/record/membrane?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 
 // 项目药剂设计:拉取设计列表  药剂类型 pac|nob|hci|sbs|anti
@@ -394,13 +412,13 @@ export async function queryDrugList(data) {
   let res = await request(
     `/api/simulations/v1/design/drug/list?${stringify(data)}`,
   );
-  return res;
+  return res?.data;
 }
 
 // 运行记录:拉取药剂记录  药剂类型 pac|nob|hci|sbs|anti
 export async function queryDrug(data) {
   let res = await request(`/api/simulations/v1/record/drug?${stringify(data)}`);
-  return res;
+  return res?.data;
 }
 
 // 子任务列表
@@ -414,3 +432,24 @@ export async function queryMandate(data) {
   let res = await request(`/api/v1/mandate/info?${stringify(data)}`);
   return res;
 }
+
+export async function getDeviceRealDataByTime(params, signal) {
+  // params.size = 999999;
+  return request(`/jinke-cloud/db/device/history-data?${stringify(params)}`, {
+    // return request(`/jinke-cloud/device/history-data?${stringify(params)}`, {
+    method: 'GET',
+    signal,
+  });
+}
+
+/**
+ * 获取历史数据
+ * @param {{project_id: string, start_time: string, end_time: string, page: string, page_size: string}} params
+ * @returns
+ */
+export async function getHistoryRecord(params) {
+  return await request(`/api/analysis/v1/io/list`, {
+    method: 'POST',
+    data: params,
+  });
+}

+ 4 - 4
src/services/TaskManage.js

@@ -41,7 +41,7 @@ export async function getMandateDetail(params) {
 export async function setTaskAutomation(params) {
   return request(`${baseURL}/v1/mandate/automation`, {
     method: 'POST',
-    body: params,
+    data: params,
   });
 }
 
@@ -57,7 +57,7 @@ export async function ignoreTask(params) {
   return request(`${baseURL}/v1/mandate/edit`, {
     method: 'POST',
     headers: { ContentType: 'application/x-www-form-urlencoded' },
-    body: encodeParams,
+    data: encodeParams,
   });
 }
 
@@ -74,7 +74,7 @@ export async function ignoreTask(params) {
 export async function dispatchOrder(params) {
   return request(`${baseURL}/v1/work_order/save`, {
     method: 'POST',
-    body: params,
+    data: params,
   });
 }
 
@@ -119,5 +119,5 @@ export async function getCraftRecordList(data) {
 
 export async function getDiagnosticDetail(detailId) {
   const res = await request(`/api/v1/dumu/detail/${detailId}`);
-  return res.data
+  return res.data;
 }

+ 37 - 0
src/services/diagnosticTec.js

@@ -0,0 +1,37 @@
+import { stringify } from 'qs';
+import { request } from 'umi';
+
+export async function queryProject(params = {}) {
+  return request(`/api/v2/project?${stringify(params)}`);
+}
+
+export async function queryStatisticList(params = {}) {
+  return request(
+    `/fault_analysis/result-count/${params.projectId}?${stringify(params)}`,
+  );
+}
+
+export async function queryList(params = {}) {
+  return request(
+    `/fault_analysis/result-list/${params.projectId}?${stringify(params)}`,
+  );
+}
+export async function queryListNew(params = {}) {
+  return request(`/api/analysis/v1/analysis-result/list`, {
+    method: 'POST',
+    data: params,
+  });
+}
+export async function queryCountNew(params = {}) {
+  return request(`/api/analysis/v1/analysis-count/list`, {
+    method: 'POST',
+    data: params,
+  });
+}
+//导出
+export async function queryAnalysisExport(params = {}) {
+  return request(`/api/analysis/v1/analysis-result/export`, {
+    method: 'POST',
+    data: params,
+  });
+}

+ 55 - 0
src/services/dumu.js

@@ -0,0 +1,55 @@
+import { stringify } from 'qs';
+import { request } from 'umi';
+
+export async function getList(projectId) {
+  const res = await request(`/api/v1/dumu/pull-msg/${projectId}`);
+  res.data.forEach((item) => {
+    item.url = base64ToImageUrl(item.event_bg);
+  });
+  return res.data;
+}
+
+export async function getAlarmList(data) {
+  const res = await request(`/api/task/v1/grade-alarm/list`, {
+    method: 'POST',
+    data,
+  });
+  return res.data;
+}
+
+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;
+}
+
+export async function getHistoryList(projectId, params) {
+  const res = await request(
+    `/api/v1/dumu/list/${projectId}?${stringify(params)}`,
+  );
+  res.data.list.forEach((item) => {
+    item.url = base64ToImageUrl(item.event_bg);
+  });
+  return res.data.list;
+}
+
+export async function getCameraList(projectId, params) {
+  const res = await request(
+    `/api/v1/dumu/list/${projectId}?${stringify(params)}`,
+  );
+  return res.data;
+}
+
+export async function getDetail(detailId, options) {
+  const res = await request(`/api/v1/dumu/detail/${detailId}`, options);
+  return res.data;
+}