Browse Source

驾驶舱

xjj 2 years ago
parent
commit
4f3c05833e
45 changed files with 4176 additions and 830 deletions
  1. 2 0
      package.json
  2. 34 0
      public/echarts.min.js
  3. 46 0
      src/Project/pages/DataMeter/CreateMould.tsx
  4. 199 0
      src/Project/pages/DataMeter/DrawerLeft.tsx
  5. 8 31
      src/Project/pages/DataMeter/Model/AlarmCenter.tsx
  6. 36 23
      src/Project/pages/DataMeter/Model/DataCenter/index.tsx
  7. 0 649
      src/Project/pages/DataMeter/Model/DataCenter/utils.js
  8. 463 0
      src/Project/pages/DataMeter/Model/DataCenter/utils.ts
  9. 77 0
      src/Project/pages/DataMeter/Model/FileManagement.less
  10. 72 0
      src/Project/pages/DataMeter/Model/FileManagement.tsx
  11. 280 0
      src/Project/pages/DataMeter/Model/Other.tsx
  12. 369 0
      src/Project/pages/DataMeter/Model/PlanManagement.tsx
  13. 284 0
      src/Project/pages/DataMeter/Model/filemanager-connector/api.js
  14. 2 0
      src/Project/pages/DataMeter/Model/filemanager-connector/apiOptions.js
  15. 92 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/create-folder.js
  16. 78 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/delete-resource.js
  17. 142 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/download.js
  18. 41 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/index.js
  19. 92 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/rename.js
  20. 137 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/see.js
  21. 28 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/sort.js
  22. 126 0
      src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/upload.js
  23. 21 0
      src/Project/pages/DataMeter/Model/filemanager-connector/icons-svg.js
  24. 39 0
      src/Project/pages/DataMeter/Model/filemanager-connector/icons.js
  25. 14 0
      src/Project/pages/DataMeter/Model/filemanager-connector/index.js
  26. 189 0
      src/Project/pages/DataMeter/Model/filemanager-connector/list-view-layout.js
  27. 280 0
      src/Project/pages/DataMeter/Model/filemanager-connector/translations.js
  28. 18 0
      src/Project/pages/DataMeter/Model/filemanager-connector/utils/common.js
  29. 49 0
      src/Project/pages/DataMeter/Model/filemanager-connector/utils/download.js
  30. 61 0
      src/Project/pages/DataMeter/Model/filemanager-connector/utils/notifications.js
  31. 28 0
      src/Project/pages/DataMeter/Model/filemanager-connector/utils/onFailError.js
  32. 32 0
      src/Project/pages/DataMeter/Model/filemanager-connector/utils/see.js
  33. 27 0
      src/Project/pages/DataMeter/Model/filemanager-connector/utils/upload.js
  34. 10 0
      src/Project/pages/DataMeter/Model/filemanager-connector/view-layout-options.js
  35. 18 72
      src/Project/pages/DataMeter/Model/index.tsx
  36. 89 0
      src/Project/pages/DataMeter/MouldDrawerLeft.tsx
  37. 1 0
      src/Project/pages/DataMeter/index.less
  38. 115 37
      src/Project/pages/DataMeter/index.tsx
  39. 8 3
      src/Project/pages/DataMeter/typings.d.ts
  40. 86 3
      src/Project/services/DataMeter.ts
  41. 10 0
      src/Project/services/FileAdmin.ts
  42. 79 2
      src/Project/services/project.ts
  43. 5 0
      src/Project/utils/index.ts
  44. 17 1
      src/models/project.ts
  45. 372 9
      yarn.lock

+ 2 - 0
package.json

@@ -13,6 +13,8 @@
   "dependencies": {
     "@ant-design/icons": "^4.7.0",
     "@ant-design/pro-components": "^2.0.1",
+    "@opuscapita/react-filemanager": "^1.1.0-beta.6",
+    "@opuscapita/react-filemanager-connector-node-v1": "^1.1.0-beta.6",
     "@types/react-grid-layout": "^1.3.2",
     "@umijs/max": "^4.0.41",
     "antd": "^5.0.0",

File diff suppressed because it is too large
+ 34 - 0
public/echarts.min.js


+ 46 - 0
src/Project/pages/DataMeter/CreateMould.tsx

@@ -0,0 +1,46 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Form, Input, Modal } from 'antd';
+import { useForm } from 'antd/es/form/Form';
+
+function CreateMould(props: DataMeter.ICreateMouldProps) {
+  const { visible, handleCancel, handleOk } = props;
+  const [form] = useForm();
+  const {} = form;
+  const layout = {
+    labelCol: { span: 5 },
+    wrapperCol: { span: 15 },
+  };
+
+  const OnOk = () => {
+    form.validateFields().then(handleOk);
+  };
+
+  return (
+    <Modal
+      title="创建模板"
+      destroyOnClose
+      open={visible}
+      onOk={OnOk}
+      width="40%"
+      maskClosable={false}
+      okText="新建"
+      onCancel={handleCancel}
+    >
+      <Form form={form} {...layout}>
+        <Form.Item
+          name={'name'}
+          label="模板名称"
+          rules={[
+            {
+              required: true,
+              message: '请填写模板名称',
+            },
+          ]}
+        >
+          <Input placeholder="请输入模板名称"></Input>
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+}
+export default CreateMould;

+ 199 - 0
src/Project/pages/DataMeter/DrawerLeft.tsx

@@ -0,0 +1,199 @@
+import React, { useMemo, useState } from 'react';
+import { List, Collapse } from 'antd';
+import styles from './index.less';
+
+var currentPosition: any = { x: null, y: null, top: 0, right: 0 };
+const { Panel } = Collapse;
+
+function DrawerLeft(props: any) {
+  const {
+    onClose,
+    visible,
+    subModule,
+    dataCenterChild,
+    layout,
+    addModel,
+    removeModel,
+  } = props;
+  const [position, setPosition] = useState({ top: 0, right: 0 });
+  const [dataCenterVisible, setDataCenterVisible] = useState(false);
+  const [rotate, setRotate] = useState('');
+
+  const modelList = useMemo(() => {
+    const arr = [
+      {
+        title: '项目概况',
+        key: 'ProjectInfo',
+      },
+      {
+        title: '报警中心',
+        key: 'AlarmCenter',
+      },
+      {
+        title: '消息中心',
+        key: 'MessageCenter',
+      },
+      {
+        title: '文件管理',
+        key: 'FileManagement',
+      },
+      {
+        title: '视频监控',
+        key: 'Monitor',
+      },
+      {
+        title: '数据中心',
+        key: 'DataCenter',
+      },
+      {
+        title: '计划管理类',
+        key: 'PlanManagement',
+      },
+      {
+        title: '其它',
+        key: 'Other',
+      },
+    ];
+    if (subModule) {
+      return arr.filter((item) => {
+        switch (item.key) {
+          case 'ProjectInfo':
+          case 'Monitor':
+          case 'FileManagement':
+          case 'PlanManagement':
+            return false;
+        }
+      });
+    }
+    return arr;
+  }, [subModule]);
+
+  const isActive = (item: any) => {
+    if (
+      layout.find((cur: any) => item.key == cur.key || item.key == cur.active)
+    )
+      return true;
+    return false;
+  };
+  const getDataCenterChild = () => {
+    return (
+      <div style={{ width: '100%' }}>
+        <div
+          style={{ display: 'flex', alignItems: 'center' }}
+          onClick={() => {
+            setRotate(dataCenterVisible ? '' : 'rotate(90deg)');
+            setDataCenterVisible(!dataCenterVisible);
+          }}
+        >
+          <div style={{ transform: rotate }} className={styles.icon} />
+          <span className={styles.title}>数据中心</span>
+        </div>
+        <div style={{ paddingLeft: '20px' }}>
+          {dataCenterVisible && getItems(dataCenterChild, 'DataCenter')}
+        </div>
+      </div>
+    );
+  };
+  const getItems = (arr: any, name: any) => {
+    return arr.map((item: any) => {
+      let key = `${name};;${item.key}`;
+      if (item.children) {
+        return (
+          <Collapse bordered={false}>
+            <Panel
+              header={
+                <div onMouseDown={(e) => e.stopPropagation()}>{item.title}</div>
+              }
+              key="1"
+            >
+              {getItems(item.children, key)}
+            </Panel>
+          </Collapse>
+        );
+      } else {
+        return (
+          <div
+            className={styles.itemDiv}
+            onClick={() => {
+              isActive(item)
+                ? removeModel('DataCenter', item.key)
+                : addModel(key);
+            }}
+          >
+            {item.title}
+            <div
+              className={
+                isActive(item) ? styles.childIconOut : styles.childIconIn
+              }
+            ></div>
+          </div>
+        );
+      }
+    });
+  };
+  const onMouseDown = (e: any) => {
+    // console.log(e);
+    currentPosition = { x: e.screenX, y: e.screenY };
+
+    document.addEventListener('mousemove', doMouseMove);
+    document.addEventListener('mouseup', doMouseUp);
+  };
+  const doMouseMove = (e: any) => {
+    setPosition({
+      top: position.top + e.screenY - currentPosition.y,
+      right: position.right + currentPosition.x - e.screenX,
+    });
+  };
+  const doMouseUp = () => {
+    document.removeEventListener('mousemove', doMouseMove);
+    document.removeEventListener('mouseup', doMouseUp);
+  };
+
+  if (!visible) return null;
+  return (
+    <div className={styles.drawer} style={position}>
+      <div className={styles.header} onMouseDown={onMouseDown}>
+        模块列表
+        <div className={styles.closeIcon} onClick={onClose} />
+      </div>
+      <div className={styles.content}>
+        <div className={styles.titleTip}>
+          <div className={styles.iconIn} />
+          <div className={styles.titleTipText}>添加</div>
+          <div className={styles.titleTipLine}></div>
+          <div className={styles.iconOut} />
+          <div className={styles.titleTipText}>移除</div>
+        </div>
+        <List
+          bordered
+          dataSource={modelList}
+          renderItem={(item) => (
+            <List.Item style={{ cursor: 'pointer', userSelect: 'none' }}>
+              {item.key == 'DataCenter' ? (
+                getDataCenterChild()
+              ) : (
+                <div
+                  className={styles.itemContent}
+                  onClick={() => {
+                    isActive(item)
+                      ? removeModel(item.key, null)
+                      : addModel(item.key);
+                  }}
+                >
+                  <div className={styles.title}>{item.title}</div>
+                  <div
+                    className={
+                      isActive(item) ? styles.itemIconOut : styles.itemIconIn
+                    }
+                  />
+                </div>
+              )}
+            </List.Item>
+          )}
+        />
+      </div>
+    </div>
+  );
+}
+
+export default DrawerLeft;

+ 8 - 31
src/Project/pages/DataMeter/Model/AlarmCenter.tsx

@@ -12,6 +12,7 @@ import {
   getPatrolRecord,
   getProjectList,
 } from '@/Project/services/DataMeter';
+import { useModel } from '@umijs/max';
 
 const { Option } = Select;
 
@@ -23,6 +24,8 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
   const [showTabs, setShowTabs] = useState(false);
   const [title, setTitle] = useState('');
   const [faultActive, setFaultActive] = useState(1);
+  const { getProject } = useModel('project');
+
   const id = projectId || -1;
   const projectAlarmRequest = useRequest(getProjectAlarm, {
     defaultParams: [
@@ -65,17 +68,11 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
       },
     ],
   });
-  const projectRequest = useRequest(getProjectList, {
-    cacheKey: 'projectList',
-    staleTime: -1,
-  },
-);
   const projectAlarmList = projectAlarmRequest.data?.list || [];
   const issueList = IssueListRequest.data?.list || [];
   const faultAnalysis = faultAnalysisRequest.data || [];
   const breakdownList = breakdownRecordRequest.data?.list || [];
   const patrolList = patrolRecordRequest.data?.list || [];
-  const projectList = projectRequest.data?.list || [];
 
   const getTitle = (title: string) => {
     return (
@@ -259,11 +256,7 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
                     }}
                     className={style.alarmTitle}
                   >
-                    {getTitle(
-                      projectList.find(
-                        (project: any) => project.ID == item.ProjectId,
-                      )?.Name || '无',
-                    )}
+                    {getTitle(getProject(item.ProjectId)?.Name || '无')}
                   </td>
                 )}
               </tr>
@@ -378,11 +371,7 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
                     style={{ width: '40%', paddingRight: '10px' }}
                     className={style.alarmTitle}
                   >
-                    {getTitle(
-                      projectList.find(
-                        (project: any) => project.ID == item.ProjectId,
-                      )?.Name || '无',
-                    )}
+                    {getTitle(getProject(item.ProjectId)?.Name || '无')}
                   </td>
                 )}
               </tr>
@@ -496,11 +485,7 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
                   style={{ width: '40%', paddingRight: '10px' }}
                   className={style.alarmTitle}
                 >
-                  {getTitle(
-                    projectList.find(
-                      (project: any) => project.ID == item?.Project?.ID,
-                    )?.Name || '无',
-                  )}
+                  {getTitle(getProject(item?.Project?.ID)?.Name || '无')}
                 </td>
               )}
             </tr>
@@ -628,11 +613,7 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
                   style={{ width: '30%', paddingRight: '10px' }}
                   className={style.alarmTitle}
                 >
-                  {getTitle(
-                    projectList.find(
-                      (project: any) => project.ID == item.project_id,
-                    )?.Name || '无',
-                  )}
+                  {getTitle(getProject(item.project_id)?.Name || '无')}
                 </td>
               )}
             </tr>
@@ -819,11 +800,7 @@ function AlarmCenter(props: DataMeter.IModelsProps) {
                   style={{ width: '40%', paddingRight: '10px' }}
                   className={style.alarmTitle}
                 >
-                  {getTitle(
-                    projectList.find(
-                      (project: any) => project.ID == item.project_id,
-                    )?.Name || '无',
-                  )}
+                  {getTitle(getProject(item.project_id)?.Name || '无')}
                 </td>
               )}
             </tr>

+ 36 - 23
src/Project/pages/DataMeter/Model/DataCenter/index.tsx

@@ -1,7 +1,7 @@
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, useMemo } from 'react';
 import { Button, Empty, Spin } from 'antd';
-import echarts from 'echarts';
-import style from './index.less';
+import * as echarts from 'echarts';
+import style from '../../index.less';
 import { ChartBoxTitle } from '../ChartBox';
 import {
   defaultOptions,
@@ -15,8 +15,9 @@ import {
 } from './chartConfig';
 import { useRequest } from '@umijs/max';
 import { queryConfigList } from '@/Project/services/DataMeter';
+import { CHILD_MAP } from '../../config';
 
-const { getOptions2: queryOptions } = require('./utils');
+import { getOptions2 as queryOptions } from './utils';
 
 function DataCenter(props: DataMeter.IModelsProps) {
   const [chart, setChart] = useState<echarts.ECharts>();
@@ -28,9 +29,22 @@ function DataCenter(props: DataMeter.IModelsProps) {
   const chartEle = useRef<HTMLDivElement | null>(null);
   const iframeRef = useRef<HTMLIFrameElement | null>(null);
   const signalRef = useRef<any>();
-  // const [loading, setLoading] = useState(false);
-  const { child, setActive, projectId, layout } = props;
+  const [loading, setLoading] = useState(false);
+  const { child: ajaxChild, setActive, projectId, layout, subModule } = props;
   const { h, w } = layout;
+
+  const child = useMemo(() => {
+    let child = ajaxChild.map((config: any) => ({
+      ...config,
+      title: config.name,
+      key: config.id + CHILD_MAP.DataCenter.length,
+      type: 'chartConfig',
+      show: true,
+    }));
+
+    return CHILD_MAP.DataCenter.concat(child);
+  }, [ajaxChild]);
+
   const [active, setSelfActive] = useState(
     layout.active ||
       child.find((item: DataMeter.ILayoutChild) => item.show)?.key,
@@ -65,13 +79,13 @@ function DataCenter(props: DataMeter.IModelsProps) {
     },
   });
 
-  const queryOptionsRequest = useRequest(queryOptions, {
-    manual: true,
-    onSuccess(data, params) {
-      console.log('=================renderChart=======', data);
-      iframeRef.current?.contentWindow?.render(data);
-    },
-  });
+  // const queryOptionsRequest = useRequest(queryOptions, {
+  //   manual: true,
+  //   onSuccess(data, params) {
+  //     console.log('=================renderChart=======', data);
+  //     // iframeRef.current?.contentWindow?.render(data);
+  //   },
+  // });
 
   // 旧版图表
   const getOptionsForConfig = (item: DataMeter.ILayoutChild) => {
@@ -147,15 +161,15 @@ function DataCenter(props: DataMeter.IModelsProps) {
     const datas = chartOptions.configs;
     const values = chartOptions.options;
     const formula = JSON.parse(chartOptions.formula || '[]');
-    // setLoading(true);
+    setLoading(true);
     try {
-      queryOptionsRequest.run(values, datas, formula, projectId);
-      // const options = await queryOptions(values, datas, formula, projectId);
-      // iframeRef.current?.contentWindow?.render(options);
+      // queryOptionsRequest.run(values, datas, formula, projectId);
+      const options = await queryOptions(values, datas, formula, projectId);
+      iframeRef.current?.contentWindow?.render(options);
     } catch (error) {
       // console.log(error);
     }
-    // setLoading(false);
+    setLoading(false);
   };
 
   useEffect(() => {
@@ -180,7 +194,7 @@ function DataCenter(props: DataMeter.IModelsProps) {
   }, [h, w, showTabs]);
 
   useEffect(() => {
-    if (!chart || child.length == 0) return;
+    if (!chart || ajaxChild.length == 0) return;
     let { active } = layout;
     if (!active) active = child[0].key;
     handleClickTabs(active);
@@ -190,7 +204,7 @@ function DataCenter(props: DataMeter.IModelsProps) {
     if (current) {
       setTitle(current.title);
     }
-  }, [layout.active, chart, child.length]);
+  }, [layout.active, chart, ajaxChild.length]);
 
   useEffect(() => {
     const chartWindow = iframeRef.current?.contentWindow;
@@ -213,7 +227,7 @@ function DataCenter(props: DataMeter.IModelsProps) {
         width={layout.w}
       />
       {showTabs && (
-        <Spin spinning={configRequest.loading || queryOptionsRequest.loading}>
+        <Spin spinning={configRequest.loading || loading}>
           <ul className={style.tabsList}>
             {(child || [])
               .filter((item) => item.show)
@@ -248,7 +262,7 @@ function DataCenter(props: DataMeter.IModelsProps) {
         <div
           ref={chartEle}
           style={{
-            // display: pfdOptions || chartOptions,
+            display: chartOptions,
             height: '100%',
           }}
         />
@@ -300,4 +314,3 @@ export default DataCenter;
 //     </div>
 //   );
 // };
-

+ 0 - 649
src/Project/pages/DataMeter/Model/DataCenter/utils.js

@@ -1,649 +0,0 @@
-import moment from 'moment';
-import { computePrefixExpression } from './compute';
-
-import {
-  getDeviceRealData,
-  getDeviceRealDataByTime,
-  getDeviceRealData2,
-  getExcelUrl,
-  queryFormCurrentData,
-  queryFormHistoryData,
-  queryFormHistoryData2,
-} from '@/services/ProjectAdmin';
-export async function getOptions(values, datas, formula, projectId) {
-  let plcDatas = [],
-    formDatas = [];
-  datas.forEach((item, index) => {
-    item.index = index;
-    if (item.data_type == 0) {
-      plcDatas.push(item);
-    } else {
-      formDatas.push(item);
-    }
-  });
-
-  var plcData = await getPlcOptions(values, plcDatas, formula, projectId);
-  var formData = await getFormOptions(values, formDatas, projectId);
-
-  if (values.timeType) {
-    // 获得时间轴全集
-    let times = getTimes(plcData[0]?.data, formData[0]?.data);
-
-    // 根据时间全集补填数据
-    formatData(plcData, times);
-    formatData(formData, times);
-  }
-  // console.log(formData, plcData);
-  return {
-    ...values,
-    // 合并data
-    data: plcData.concat(formData),
-  };
-}
-
-function getTimes(plcTimes = [], formTimes = []) {
-  let times = {};
-
-  plcTimes.forEach(item => {
-    times[item.htime] = true;
-  });
-  formTimes.forEach(item => {
-    times[item.htime] = true;
-  });
-  return Object.keys(times).sort((a, b) => new Date(a) - new Date(b));
-}
-
-// 根据时间进行格式化数据
-function formatData(datas, times) {
-  datas.forEach(item => {
-    // 默认数据
-    let newItemData = [];
-    let i = 0;
-    times.forEach(t => {
-      let curData = item.data.find(item => item.htime == t);
-      if (!curData) {
-        // 空缺时间要补0
-        newItemData.push({
-          htime: t,
-          val: 0,
-        });
-      } else {
-        newItemData.push(curData);
-      }
-    });
-    // 使用新值
-    item.data = newItemData;
-  });
-}
-
-/**
- * 获取图表的options
- * @param {object} values 表单数据
- * @param {array} datas 数据项
- * @param {array} formula 数据公式
- * @returns
- */
-export async function getPlcOptions(values, datas, formula, project_id) {
-  let arrtData = values.data || [];
-  let params = getSingleData(datas, project_id);
-  // let multiParams = getFormula(formula);
-  if (!values.timeType) {
-    let res = await getData(params, values);
-    // for (let i = 0; i < multiParams.length; i++) {
-    //   let device = multiParams[i].paramsDevice;
-    //   let indexArr = multiParams[i].indexArr;
-    //   let tempExpression = [...multiParams[i].expression];
-    //   let formulaRes = await getData(device, values);
-    //   formulaRes.data.map(res => {
-    //     let temp = device.find(child => child.deviceName === res.alias);
-    //     // console.log(temp);
-    //     if (temp) {
-    //       var indexObj = indexArr.find(item => item.deviceName === temp.deviceName);
-    //       if (indexObj) {
-    //         tempExpression[indexObj.index] = res.val;
-    //       }
-    //     }
-    //   });
-    //   let tempValue = computePrefixExpression([...tempExpression]);
-    //   multiParams[i].formatExpression = tempExpression;
-    //   multiParams[i].value = tempValue;
-    // }
-    let resData = res.data.map((item, index) => {
-      let attrDataItem = arrtData[index + formula.length] || {};
-      return {
-        ...attrDataItem,
-        name: item.alias,
-        value: item.val * 1,
-      };
-    });
-    // let formulaData = multiParams.map((item, index) => {
-    //   let attrDataItem = arrtData[index] || {};
-    //   return {
-    //     ...attrDataItem,
-    //     name: item.FormulaName,
-    //     value: item.value * 1,
-    //   };
-    // });
-
-    // return [...resData, ...formulaData];
-    return resData;
-  } else {
-    let singleData = [];
-    for (let i = 0; i < params.length; i++) {
-      const item = params[i];
-      let res = await getData(item, values);
-      // let attrDataItem = arrtData[i + multiParams.length] || {};
-      let attrDataItem = arrtData[i + formula.length] || {};
-      singleData.push({
-        ...attrDataItem,
-        name: item.deviceName,
-        data: (res.data || []).map(item => {
-          return {
-            val: item.val,
-            htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
-          };
-        }),
-      });
-    }
-
-    // let firstHTimeArr = [];
-    // let formulaData = [];
-    // for (let i = 0; i < multiParams.length; i++) {
-    //   const element = multiParams[i];
-    //   let indexArr = element.indexArr;
-    //   let tempExpression = [...element.expression];
-    //   let device = element.paramsDevice;
-    //   let child = {};
-    //   child.tempExpression = tempExpression;
-    //   child.name = element.FormulaName;
-    //   child.data = [];
-    //   child.indexArr = indexArr;
-    //   for (let j = 0; j < device.length; j++) {
-    //     const tempDevice = device[j];
-    //     let res = await getData(tempDevice, values);
-    //     child.data.push({
-    //       data: res.data,
-    //       device: tempDevice,
-    //     });
-    //   }
-    //   formulaData.push(child);
-    // }
-
-    // let data = [];
-
-    // formulaData.map((item, index) => {
-    //   let attrDataItem = arrtData[index] || {};
-    //   data.push({ ...attrDataItem, ...getFunctionValue(item) });
-    // });
-
-    // return [...singleData, ...data];
-    return singleData;
-  }
-}
-// 根据数据项获取请求参数
-function getSingleData(datas, project_id) {
-  let params = [];
-  datas.forEach(({ device_id, device_items, seq }) => {
-    if (device_id && device_items) {
-      params.push({
-        deviceName: seq,
-        deviceId: device_id,
-        deviceItems: device_items,
-        project_id: Number(project_id),
-      });
-    }
-  });
-  return params;
-}
-// 根据公式获得请求参数
-function getFormula(formula) {
-  let params = [];
-  formula.forEach(item => {
-    let tempItem = item;
-    let tempDeviceArr = [];
-    let indexArr = [];
-    item.params.map(item => {
-      if (item.data_type == 1) {
-      } else if (item.Id && item.ItemAlias && item.ItemName) {
-        tempDeviceArr.push({
-          deviceName: item.ItemAlias,
-          deviceId: String(item.PlcDeviceId),
-          deviceItems: item.ItemName,
-        });
-
-        indexArr.push({ index: item.index, deviceName: item.ItemAlias });
-      }
-    });
-    tempItem.paramsDevice = tempDeviceArr;
-    tempItem.indexArr = indexArr;
-    params.push(tempItem);
-  });
-  return params;
-}
-
-function getFunctionValue(child) {
-  let data = {};
-  data.data = [];
-  let expression = child.tempExpression;
-  let first = child.data[0];
-  let indexArr = child.indexArr;
-  first.data.forEach(item => {
-    // firstDevice.device
-    let resObj = {
-      [first.device.deviceName]: item,
-    };
-    let htime = item.htime;
-    for (let i = 1; i < child.data.length; i++) {
-      let element = child.data[i];
-      // deviceName = element.name
-      let result = element.data.find(item => item.htime === htime);
-      if (!result) return;
-      resObj[element.device.deviceName] = result;
-    }
-    Object.keys(resObj).forEach(key => {
-      var result = indexArr.find(item => key === item.deviceName);
-      if (result) expression[result.index] = resObj[key].val;
-    });
-    // console.log(expression);
-    data.data.push({
-      htime: moment(htime).format('YYYY-MM-DD HH:mm:ss'),
-      val: computePrefixExpression([...expression]),
-    });
-    // console.log(indexArr, resObj, expression);
-    //  indexArr   resObj     expression
-  });
-  data.name = child.name;
-  return data;
-}
-
-var DATA_CACHE = {};
-// 请求plc数据
-async function getData(params, values) {
-  const { timeType, date, size, interval, aggregator } = values;
-  let key, etime, stime;
-  if (!timeType) {
-    // key = `${params.map(item => item.deviceItems).join(',')}-${timeType}`;
-  } else if (timeType != -1) {
-    // key = `${params.deviceItems}-${timeType}-${size}-${interval}-${aggregator}`;
-  } else {
-    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
-    etime = moment(date[1]).set(clear) * 1;
-    stime = moment(date[0]).set(clear) * 1;
-    // key = `${params.deviceItems}-${stime}-${etime}-${size}-${interval}-${aggregator}`;
-  }
-  // if (!DATA_CACHE[key]) {
-  if (!timeType) {
-    // DATA_CACHE[key] = await getDeviceRealData(params);
-    return await getDeviceRealData(params);
-  } else {
-    if (timeType != -1) {
-      let currentDate = moment();
-      etime = currentDate * 1;
-      stime = currentDate.add(-1 * timeType, 'hour') * 1;
-    }
-    return await getDeviceRealDataByTime({
-      deviceid: params.deviceId * 1,
-      dataitemid: params.deviceItems,
-      project_id: Number(params.project_id),
-      stime,
-      etime,
-      size,
-      interval,
-      aggregator,
-    });
-  }
-  // }
-  // return DATA_CACHE[key];
-}
-
-// 请求plc历史数据
-async function getDataHistory(params, values) {
-  const { timeType, size, interval, aggregator, date } = values;
-  let currentDate = moment();
-  let etime;
-  let stime;
-  let res = [];
-  if (timeType != -1) {
-    let currentDate = moment();
-    etime = currentDate * 1;
-    stime = currentDate.add(-1 * timeType, 'hour') * 1;
-  } else {
-    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
-    etime = moment(date[1]).set(clear) * 1;
-    stime = moment(date[0]).set(clear) * 1;
-  }
-  for (var i = 0; i < params.length; i++) {
-    const item = params[i];
-    const { data } = await getDeviceRealDataByTime({
-      deviceid: item.deviceId * 1,
-      dataitemid: item.deviceItems,
-      project_id: Number(item.project_id),
-      stime,
-      etime,
-      size,
-      interval,
-      aggregator,
-    });
-    res.push({
-      paramsInfo: item,
-      name: item.deviceName,
-      data: (data || [])
-        .filter(item => item.htime_at)
-        .map(item => {
-          return {
-            val: item.val * 1,
-            htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
-          };
-        }),
-    });
-  }
-  return res;
-}
-
-// 获取表单的options
-export async function getFormOptions(values, datas, projectId) {
-  let arrtData = values.data || [];
-  const params = getFormParams(datas, projectId);
-  if (!values.timeType) {
-    // 请求最新数据
-    let data = await getFormCurrentData(params, values);
-    let resData = data.map((item, index) => {
-      let attrDataItem = arrtData[index] || {};
-      return {
-        ...attrDataItem,
-        name: item.title,
-        value: item.value * 1,
-      };
-    });
-    return resData;
-  } else {
-    // 请求历史数据
-    let data = await getFormHistoryData(params, values);
-    return data.map((item, i) => {
-      let attrDataItem = arrtData[i] || {};
-      return {
-        ...attrDataItem,
-        ...item,
-      };
-    });
-  }
-}
-
-// 根据表单的数据项获取请求参数
-function getFormParams(datas, projectId) {
-  let params = {};
-  datas.forEach(item => {
-    if (!params[item.data_name]) params[item.data_name] = [];
-    params[item.data_name].push(item.data_title);
-  });
-  return Object.keys(params).map(data_name => ({
-    formName: data_name,
-    titles: params[data_name],
-    projectId,
-  }));
-}
-
-// 请求表单最新数据
-async function getFormCurrentData(params, values) {
-  const { timeType, date } = values;
-  let data = [];
-  for (let i = 0; i < params.length; i++) {
-    const resData = await queryFormCurrentData(params[i]);
-    data = [...data, ...resData];
-  }
-  return data;
-}
-
-// 请求表单历史数据
-async function getFormHistoryData(params, values) {
-  const { timeType, date, size, interval, aggregator } = values;
-  let sTime, eTime;
-  let data = [];
-  // -1为自选日期  从date内获取时间
-  if (timeType == -1) {
-    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
-    eTime = moment(date[1])
-      .set(clear)
-      .format('YYYY-MM-DD HH:mm');
-    sTime = moment(date[0])
-      .set(clear)
-      .format('YYYY-MM-DD HH:mm');
-  } else {
-    let currentDate = moment();
-    eTime = currentDate.format('YYYY-MM-DD HH:mm');
-    sTime = currentDate.add(-1 * timeType, 'hour').format('YYYY-MM-DD HH:mm');
-  }
-
-  // for (let i = 0; i < params.length; i++) {
-  //   const resData = await queryFormHistoryData({ ...params[i], eTime, sTime });
-  //   data = [...data, ...resData];
-  // }
-  const item_info = params.map(item => ({
-    table: item.formName,
-    pro: item.titles.map(title => ({ key: title })),
-  }));
-  let queryParams = {
-    project_id: Number(params[0].projectId),
-    item_info,
-    stime: sTime,
-    etime: eTime,
-    interval,
-    size: Number(size),
-    aggregator,
-  };
-  const resData = await queryFormHistoryData2(queryParams);
-
-  return resData;
-}
-
-async function queryData(values, datas, projectId) {
-  let plcDatas = [],
-    formDatas = [];
-  datas.forEach(item => {
-    if (item.data_type == 0) {
-      plcDatas.push(item);
-    } else {
-      formDatas.push(item);
-    }
-  });
-
-  // 根据params获取form的数据
-  var formData = await getFormData(values, formDatas, projectId);
-  var plcData = await getPlcData(values, plcDatas, projectId);
-  if (values.timeType) {
-    // 获得时间轴全集
-    let times = getTimes(plcData[0]?.data, formData[0]?.data);
-
-    // 根据时间全集补填数据
-    formatData(plcData, times);
-    formatData(formData, times);
-  }
-  return {
-    plcData,
-    formData,
-  };
-}
-
-async function getFormData(values, datas, projectId) {
-  let arrtData = values.data || [];
-  const params = getFormParams(datas, projectId);
-  if (params.length == 0) return [];
-  if (!values.timeType) {
-    // 请求最新数据
-    return await getFormCurrentData(params, values);
-  } else {
-    // 请求历史数据
-    return await getFormHistoryData(params, values);
-  }
-}
-async function getPlcData(values, datas, project_id) {
-  let arrtData = values.data || [];
-  let params = getSingleData(datas, project_id);
-  if (!values.timeType) {
-    let res = await getData(params, values);
-    return res.data;
-  } else {
-    // let singleData = [];
-    // for (let i = 0; i < params.length; i++) {
-    //   const item = params[i];
-    //   let res = await getData(item, values);
-    //   singleData.push({
-    //     paramsInfo: item,
-    //     name: item.deviceName,
-    //     data: (res.data || [])
-    //       .filter(item => item.htime_at)
-    //       .map(item => {
-    //         return {
-    //           val: item.val * 1,
-    //           htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
-    //         };
-    //       }),
-    //   });
-    // }
-    // return singleData;
-    return await getDataHistory(params, values);
-  }
-}
-
-export async function getOptions2(values, datas, formula, projectId) {
-  let allDatas = getAllParams(datas, formula);
-  if (allDatas.length == 0) return;
-
-  let optionsData = getOptionsData(datas, formula, values);
-
-  // 请求接口
-  const { plcData, formData } = await queryData(values, allDatas, projectId);
-
-  optionsData = optionsData.map(item => {
-    const paramsInfo = item.paramsInfo;
-    let res;
-    if (paramsInfo.data_type == 0) {
-      // plc数据
-      res = findPlcData(paramsInfo, plcData, values.timeType);
-    } else if (paramsInfo.data_type == 1) {
-      // form数据
-      res = findFormData(paramsInfo, formData, values.timeType);
-    } else if (paramsInfo.data_type == 2) {
-      // 获取公式的值
-      res = getFormulaData(item.paramsInfo, plcData, formData, values.timeType);
-    }
-    return { ...item, ...res };
-  });
-  return {
-    ...values,
-    data: optionsData,
-  };
-}
-
-function findPlcData(paramsInfo, plcData, timeType) {
-  // 实时数据与历史数据结构不一致  需判断
-  if (timeType) {
-    return plcData.find(resItem => resItem.paramsInfo.deviceName == paramsInfo.seq);
-  } else {
-    let res = plcData.find(
-      resItem =>
-        resItem.itemname == paramsInfo.device_items && resItem.devid == paramsInfo.device_id
-    );
-    return {
-      name: res.alias,
-      value: res.val,
-    };
-  }
-}
-function findFormData(paramsInfo, formData, timeType) {
-  // 实时数据与历史数据结构不一致  需判断
-  if (timeType) {
-    return formData.find(resItem => resItem.name == paramsInfo.data_title);
-  } else {
-    let res = formData.find(resItem => resItem.title == paramsInfo.data_title);
-    return {
-      name: res.title,
-      value: res.value,
-    };
-  }
-}
-
-function getAllParams(datas, formula) {
-  let allDatas = [...datas];
-  formula.forEach(f => {
-    f.params.forEach(params => {
-      if (params.data_type == 0) {
-        if (!datas.find(item => item.seq == params.seq)) {
-          allDatas.push(params);
-        }
-      } else {
-        if (!datas.find(item => item.data_title == params.data_title)) {
-          allDatas.push(params);
-        }
-      }
-    });
-  });
-  return allDatas;
-}
-
-function getOptionsData(datas, formula, values) {
-  let valuesData = values.data || [];
-
-  let optionsData = [];
-  formula.forEach((item, index) => {
-    var arrData = valuesData[index] || {};
-    item.data_type = 2;
-    arrData.paramsInfo = item;
-    optionsData.push(arrData);
-  });
-  datas.forEach((data, index) => {
-    var arrData = valuesData[index + formula.length] || {};
-    arrData.paramsInfo = data;
-    optionsData.push(arrData);
-  });
-  return optionsData;
-}
-
-function getFormulaData(formula, plcData, formData, timeType) {
-  let expression = [...formula.expression];
-  let resDatas = formula.params.map(params => {
-    let res;
-    if (params.data_type == 0) {
-      res = findPlcData(params, plcData, timeType);
-      return {
-        ...params,
-        ...res,
-      };
-    } else {
-      res = findFormData(params, formData, timeType);
-      return {
-        ...params,
-        ...res,
-      };
-    }
-  });
-
-  if (timeType) {
-    let optionsData = [];
-    // 获取时间
-    let time = resDatas[0].data.map(item => item.htime);
-    time.forEach((htime, index) => {
-      resDatas.forEach(params => {
-        // 根据index去替换表达式中对应的值
-        expression[params.index] = params.data[index].val || 0;
-      });
-      optionsData.push({
-        htime: moment(htime).format('YYYY-MM-DD HH:mm:ss'),
-        val: computePrefixExpression([...expression]),
-      });
-    });
-    return {
-      data: optionsData,
-      name: formula.FormulaName,
-    };
-  } else {
-    resDatas.forEach(params => {
-      // 根据index去替换表达式中对应的值
-      expression[params.index] = params.value || 0;
-    });
-    return {
-      value: computePrefixExpression([...expression]),
-      name: formula.FormulaName,
-    };
-  }
-}

+ 463 - 0
src/Project/pages/DataMeter/Model/DataCenter/utils.ts

@@ -0,0 +1,463 @@
+import moment from 'moment';
+// import { computePrefixExpression } from './compute';
+
+import {
+  getDeviceRealData,
+  getDeviceRealDataByTime,
+  queryFormCurrentData,
+  queryFormHistoryData2,
+} from '@/Project/services/project';
+export async function getOptions(
+  values: any,
+  datas: any,
+  formula: any,
+  projectId: any,
+) {
+  let plcDatas: any = [],
+    formDatas: any = [];
+  datas.forEach((item: any, index: any) => {
+    item.index = index;
+    if (item.data_type == 0) {
+      plcDatas.push(item);
+    } else {
+      formDatas.push(item);
+    }
+  });
+
+  var plcData = await getPlcOptions(values, plcDatas, formula, projectId);
+  var formData = await getFormOptions(values, formDatas, projectId);
+
+  if (values.timeType) {
+    // 获得时间轴全集
+    let times = getTimes(plcData[0]?.data, formData[0]?.data);
+
+    // 根据时间全集补填数据
+    formatData(plcData, times);
+    formatData(formData, times);
+  }
+  // console.log(formData, plcData);
+  return {
+    ...values,
+    // 合并data
+    data: plcData.concat(formData),
+  };
+}
+
+function getTimes(plcTimes = [], formTimes = []) {
+  let times: any = {};
+
+  plcTimes.forEach((item: any) => {
+    times[item.htime] = true;
+  });
+  formTimes.forEach((item: any) => {
+    times[item.htime] = true;
+  });
+  return Object.keys(times).sort(
+    (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf(),
+  );
+}
+
+// 根据时间进行格式化数据
+function formatData(datas: any, times: any) {
+  datas.forEach((item: any) => {
+    // 默认数据
+    let newItemData: any = [];
+    let i = 0;
+    times.forEach((t: any) => {
+      let curData = item.data.find((item: any) => item.htime == t);
+      if (!curData) {
+        // 空缺时间要补0
+        newItemData.push({
+          htime: t,
+          val: 0,
+        });
+      } else {
+        newItemData.push(curData);
+      }
+    });
+    // 使用新值
+    item.data = newItemData;
+  });
+}
+
+/**
+ * 获取图表的options
+ * @param {object} values 表单数据
+ * @param {array} datas 数据项
+ * @param {array} formula 数据公式
+ * @returns
+ */
+export async function getPlcOptions(
+  values: any,
+  datas: any,
+  formula: any,
+  project_id: any,
+) {
+  let arrtData = values.data || [];
+  let params = getSingleData(datas, project_id);
+  if (!values.timeType) {
+    let res = await getData(params, values);
+    let resData = res.data.map((item: any, index: any) => {
+      let attrDataItem = arrtData[index + formula.length] || {};
+      return {
+        ...attrDataItem,
+        name: item.alias,
+        value: item.val * 1,
+      };
+    });
+    return resData;
+  } else {
+    let singleData = [];
+    for (let i = 0; i < params.length; i++) {
+      const item = params[i];
+      let res = await getData(item, values);
+      let attrDataItem = arrtData[i + formula.length] || {};
+      singleData.push({
+        ...attrDataItem,
+        name: item.deviceName,
+        data: (res.data || []).map((item: any) => {
+          return {
+            val: item.val,
+            htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
+          };
+        }),
+      });
+    }
+
+    return singleData;
+  }
+}
+// 根据数据项获取请求参数
+function getSingleData(datas: any, project_id: any) {
+  let params: any = [];
+  datas.forEach(({ device_id, device_items, seq }: any) => {
+    if (device_id && device_items) {
+      params.push({
+        deviceName: seq,
+        deviceId: device_id,
+        deviceItems: device_items,
+        project_id: Number(project_id),
+      });
+    }
+  });
+  return params;
+}
+
+// 请求plc数据
+async function getData(params: any, values: any) {
+  const { timeType, date, size, interval, aggregator } = values;
+  let key, etime, stime;
+  if (!timeType) {
+    // key = `${params.map(item => item.deviceItems).join(',')}-${timeType}`;
+  } else if (timeType != -1) {
+    // key = `${params.deviceItems}-${timeType}-${size}-${interval}-${aggregator}`;
+  } else {
+    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
+    etime = moment(date[1]).set(clear).valueOf();
+    stime = moment(date[0]).set(clear).valueOf();
+    // key = `${params.deviceItems}-${stime}-${etime}-${size}-${interval}-${aggregator}`;
+  }
+  // if (!DATA_CACHE[key]) {
+  if (!timeType) {
+    // DATA_CACHE[key] = await getDeviceRealData(params);
+    return await getDeviceRealData(params);
+  } else {
+    if (timeType != -1) {
+      let currentDate = moment();
+      etime = currentDate.valueOf();
+      stime = currentDate.add(-1 * timeType, 'hour').valueOf();
+    }
+    return await getDeviceRealDataByTime({
+      deviceid: params.deviceId * 1,
+      dataitemid: params.deviceItems,
+      project_id: Number(params.project_id),
+      stime,
+      etime,
+      size,
+      interval,
+      aggregator,
+    });
+  }
+  // }
+  // return DATA_CACHE[key];
+}
+
+// 请求plc历史数据
+async function getDataHistory(params: any, values: any) {
+  const { timeType, size, interval, aggregator, date } = values;
+  let etime;
+  let stime;
+  let res = [];
+  if (timeType != -1) {
+    let currentDate = moment();
+    etime = currentDate.valueOf();
+    stime = currentDate.add(-1 * timeType, 'hour').valueOf();
+  } else {
+    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
+    etime = moment(date[1]).set(clear).valueOf();
+    stime = moment(date[0]).set(clear).valueOf();
+  }
+  for (var i = 0; i < params.length; i++) {
+    const item = params[i];
+    const { data } = await getDeviceRealDataByTime({
+      deviceid: item.deviceId * 1,
+      dataitemid: item.deviceItems,
+      project_id: Number(item.project_id),
+      stime,
+      etime,
+      size,
+      interval,
+      aggregator,
+    });
+    res.push({
+      paramsInfo: item,
+      name: item.deviceName,
+      data: (data || [])
+        .filter((item: any) => item.htime_at)
+        .map((item: any) => {
+          return {
+            val: item.val * 1,
+            htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
+          };
+        }),
+    });
+  }
+  return res;
+}
+
+// 获取表单的options
+export async function getFormOptions(values: any, datas: any, projectId: any) {
+  let arrtData = values.data || [];
+  const params = getFormParams(datas, projectId);
+  if (!values.timeType) {
+    // 请求最新数据
+    let data = await getFormCurrentData(params);
+    let resData = data.map((item: any, index: any) => {
+      let attrDataItem = arrtData[index] || {};
+      return {
+        ...attrDataItem,
+        name: item.title,
+        value: item.value * 1,
+      };
+    });
+    return resData;
+  } else {
+    // 请求历史数据
+    let data = await getFormHistoryData(params, values);
+    return data.map((item, i) => {
+      let attrDataItem = arrtData[i] || {};
+      return {
+        ...attrDataItem,
+        ...item,
+      };
+    });
+  }
+}
+
+// 根据表单的数据项获取请求参数
+function getFormParams(datas: any, projectId: any) {
+  let params: any = {};
+  datas.forEach((item: any) => {
+    if (!params[item.data_name]) params[item.data_name] = [];
+    params[item.data_name].push(item.data_title);
+  });
+  return Object.keys(params).map((data_name) => ({
+    formName: data_name,
+    titles: params[data_name],
+    projectId,
+  }));
+}
+
+// 请求表单最新数据
+async function getFormCurrentData(params: any) {
+  let data: any = [];
+  for (let i = 0; i < params.length; i++) {
+    const resData = await queryFormCurrentData(params[i]);
+    data = [...data, ...resData];
+  }
+  return data;
+}
+
+// 请求表单历史数据
+async function getFormHistoryData(params: any, values: any) {
+  const { timeType, date, size, interval, aggregator } = values;
+  let sTime, eTime;
+  // -1为自选日期  从date内获取时间
+  if (timeType == -1) {
+    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
+    eTime = moment(date[1]).set(clear).format('YYYY-MM-DD HH:mm');
+    sTime = moment(date[0]).set(clear).format('YYYY-MM-DD HH:mm');
+  } else {
+    let currentDate = moment();
+    eTime = currentDate.format('YYYY-MM-DD HH:mm');
+    sTime = currentDate.add(-1 * timeType, 'hour').format('YYYY-MM-DD HH:mm');
+  }
+  const item_info = params.map((item: any) => ({
+    table: item.formName,
+    pro: item.titles.map((title: any) => ({ key: title })),
+  }));
+  let queryParams = {
+    project_id: Number(params[0].projectId),
+    item_info,
+    stime: sTime,
+    etime: eTime,
+    interval,
+    size: Number(size),
+    aggregator,
+  };
+  const resData = await queryFormHistoryData2(queryParams);
+
+  return resData;
+}
+
+async function queryData(values: any, datas: any, projectId: any) {
+  let plcDatas: any = [],
+    formDatas: any = [];
+  datas.forEach((item: any) => {
+    if (item.data_type == 0) {
+      plcDatas.push(item);
+    } else {
+      formDatas.push(item);
+    }
+  });
+
+  // 根据params获取form的数据
+  var formData = await getFormData(values, formDatas, projectId);
+  var plcData = await getPlcData(values, plcDatas, projectId);
+  if (values.timeType) {
+    // 获得时间轴全集
+    let times = getTimes(plcData[0]?.data, formData[0]?.data);
+
+    // 根据时间全集补填数据
+    formatData(plcData, times);
+    formatData(formData, times);
+  }
+  return {
+    plcData,
+    formData,
+  };
+}
+
+async function getFormData(values: any, datas: any, projectId: any) {
+  const params = getFormParams(datas, projectId);
+  if (params.length == 0) return [];
+  if (!values.timeType) {
+    // 请求最新数据
+    return await getFormCurrentData(params);
+  } else {
+    // 请求历史数据
+    return await getFormHistoryData(params, values);
+  }
+}
+async function getPlcData(values: any, datas: any, project_id: any) {
+  let params = getSingleData(datas, project_id);
+  if (!values.timeType) {
+    let res = await getData(params, values);
+    return res.data;
+  } else {
+    return await getDataHistory(params, values);
+  }
+}
+
+export async function getOptions2(
+  values: any,
+  datas: any,
+  formula: any,
+  projectId: any,
+) {
+  let allDatas = getAllParams(datas, formula);
+  if (allDatas.length == 0) return;
+
+  let optionsData = getOptionsData(datas, formula, values);
+
+  // 请求接口
+  const { plcData, formData } = await queryData(values, allDatas, projectId);
+
+  optionsData = optionsData.map((item: any) => {
+    const paramsInfo = item.paramsInfo;
+    let res;
+    if (paramsInfo.data_type == 0) {
+      // plc数据
+      res = findPlcData(paramsInfo, plcData, values.timeType);
+    } else if (paramsInfo.data_type == 1) {
+      // form数据
+      res = findFormData(paramsInfo, formData, values.timeType);
+    }
+    return { ...item, ...res };
+  });
+  return {
+    ...values,
+    data: optionsData,
+  };
+}
+
+function findPlcData(paramsInfo: any, plcData: any, timeType: any) {
+  // 实时数据与历史数据结构不一致  需判断
+  if (timeType) {
+    return plcData.find(
+      (resItem: any) => resItem.paramsInfo.deviceName == paramsInfo.seq,
+    );
+  } else {
+    let res = plcData.find(
+      (resItem: any) =>
+        resItem.itemname == paramsInfo.device_items &&
+        resItem.devid == paramsInfo.device_id,
+    );
+    return {
+      name: res.alias,
+      value: res.val,
+    };
+  }
+}
+function findFormData(paramsInfo: any, formData: any, timeType: any) {
+  // 实时数据与历史数据结构不一致  需判断
+  if (timeType) {
+    return formData.find(
+      (resItem: any) => resItem.name == paramsInfo.data_title,
+    );
+  } else {
+    let res = formData.find(
+      (resItem: any) => resItem.title == paramsInfo.data_title,
+    );
+    return {
+      name: res.title,
+      value: res.value,
+    };
+  }
+}
+
+function getAllParams(datas: any, formula: any) {
+  let allDatas = [...datas];
+  formula.forEach((f: any) => {
+    f.params.forEach((params: any) => {
+      if (params.data_type == 0) {
+        if (!datas.find((item: any) => item.seq == params.seq)) {
+          allDatas.push(params);
+        }
+      } else {
+        if (!datas.find((item: any) => item.data_title == params.data_title)) {
+          allDatas.push(params);
+        }
+      }
+    });
+  });
+  return allDatas;
+}
+
+function getOptionsData(datas: any, formula: any, values: any) {
+  let valuesData = values.data || [];
+
+  let optionsData: any = [];
+  formula.forEach((item: any, index: any) => {
+    var arrData = valuesData[index] || {};
+    item.data_type = 2;
+    arrData.paramsInfo = item;
+    optionsData.push(arrData);
+  });
+  datas.forEach((data: any, index: any) => {
+    var arrData = valuesData[index + formula.length] || {};
+    arrData.paramsInfo = data;
+    optionsData.push(arrData);
+  });
+  return optionsData;
+}

+ 77 - 0
src/Project/pages/DataMeter/Model/FileManagement.less

@@ -0,0 +1,77 @@
+// @import '~antd/lib/style/themes/default.less';
+//@import '~@/utils/utils.less';
+
+.tableList {
+  .tableListOperator {
+    margin-bottom: 16px;
+
+    button {
+      margin-right: 8px;
+    }
+  }
+}
+
+.tableListForm {
+  :global {
+    .ant-form-item {
+      display: flex;
+      margin-right: 0;
+      margin-bottom: 24px;
+
+      > .ant-form-item-label {
+        width: auto;
+        padding-right: 8px;
+        line-height: 32px;
+      }
+
+      .ant-form-item-control {
+        line-height: 32px;
+      }
+    }
+    .ant-form-item-control-wrapper {
+      flex: 1;
+    }
+  }
+  .submitButtons {
+    display: block;
+    margin-bottom: 24px;
+    white-space: nowrap;
+  }
+}
+
+@media screen and (max-width: 992px) {
+  .tableListForm :global(.ant-form-item) {
+    margin-right: 24px;
+  }
+}
+
+@media screen and (max-width: 768px) {
+  .tableListForm :global(.ant-form-item) {
+    margin-right: 8px;
+  }
+}
+
+.fileCon {
+  :global {
+    .oc-fm--file-navigator {
+      background: border-box;
+    }
+    .oc-fm--list-view__table {
+      color: #505458;
+    }
+    .oc-fm--file-navigator__toolbar {
+      background: #fff;
+    }
+    .oc-fm--list-view__row {
+      background: #fff;
+      border-bottom: 1px solid #e1e1e1;
+    }
+    .oc-fm--list-view__row--selected {
+      color: #fff!important;
+      background: #1890FF!important;
+    }
+    .oc-fm--no-files-found-stub {
+      display: none;
+    }
+  }
+}

+ 72 - 0
src/Project/pages/DataMeter/Model/FileManagement.tsx

@@ -0,0 +1,72 @@
+import React, { useState } from 'react';
+import { Button } from 'antd';
+// import { GetTokenFromUrl } from '@/utils/utils';
+import style from './index.less';
+// import ModelTitle from '@/components/ModelTitle';
+import { ChartBoxTitle } from './ChartBox';
+import { Empty } from 'antd';
+import { getToken } from '@/Project/utils';
+import './FileManagement.less';
+
+const {
+  FileManager,
+  FileNavigator,
+}: any = require('@opuscapita/react-filemanager');
+const connectorNodeV1: any = require('./filemanager-connector');
+
+function FileList(props: DataMeter.IModelsProps) {
+  const { projectId } = props;
+  // const { res, searchContent } = this.state;
+  const [res, setRes] = useState<any>();
+  const [searchContent, setSearchContent] = useState<any>();
+  // const [fileProp, setFileProp] = useState<any>();
+
+  const apiOptions = {
+    ...connectorNodeV1.apiOptions,
+    locale: 'cn',
+    token: getToken(),
+    searchContent: searchContent ? searchContent : '',
+    apiRoot: `/api/v1/file-service/${projectId}`, // Or you local Server Node V1 installation.
+    apiMultipleRoot: `api/v1/file-service/${projectId}`,
+  };
+  var content;
+  const OnResourceChange = (res: any) => {
+    setRes(res);
+  };
+  // const OnItemClicked = (arg: any) => {
+  //   setFileProp(arg.rowData);
+  // };
+
+  if (!projectId) {
+    content = (
+      <div className={style.modelContent}>
+        <Empty />
+      </div>
+    );
+  } else {
+    content = (
+      <FileManager>
+        <FileNavigator
+          id="water-service-file-manager"
+          api={connectorNodeV1.api}
+          apiOptions={apiOptions}
+          capabilities={() => []}
+          listViewLayout={connectorNodeV1.listViewLayout2}
+          viewLayoutOptions={connectorNodeV1.viewLayoutOptions}
+          // onResourceItemClick={OnItemClicked}
+          onResourceChange={OnResourceChange}
+          initialResourceId={searchContent ? `${res.id};${searchContent}` : ''}
+        />
+      </FileManager>
+    );
+  }
+
+  return (
+    <div className={style.modelBox}>
+      <ChartBoxTitle title={'文件管理'}></ChartBoxTitle>
+      {content}
+    </div>
+  );
+}
+
+export default FileList;

+ 280 - 0
src/Project/pages/DataMeter/Model/Other.tsx

@@ -0,0 +1,280 @@
+import React, { useState, useEffect } from 'react';
+import { Popover, Radio, Avatar, Tooltip, Empty, Table } from 'antd';
+import style from '../index.less';
+import moment from 'moment';
+import { ChartBoxTitle } from './ChartBox';
+import { useModel, useRequest } from '@umijs/max';
+import { queryProjectFileList } from '@/Project/services/FileAdmin';
+import { getDailyList } from '@/Project/services/DataMeter';
+
+function Other(props: DataMeter.IModelsProps) {
+  const { child, setActive, layout, projectId, subModule } = props;
+  const [active, setSelfActive] = useState(
+    layout.active || child.find((item) => item.show)?.key,
+  );
+  const [showTabs, setShowTabs] = useState(false);
+  const [title, setTitle] = useState('');
+  const { getProject } = useModel('project');
+  const id = projectId || -1;
+
+  const fileListRequest = useRequest(queryProjectFileList, {
+    defaultParams: [
+      {
+        fileType: 29,
+        projectId: id,
+        deviceCode: -1,
+      },
+    ],
+  });
+  const dailyListRequest = useRequest(getDailyList, {
+    defaultParams: [
+      {
+        projectId: id,
+      },
+    ],
+  });
+
+  const fileList = fileListRequest.data || [];
+  const dailyList = dailyListRequest.data?.list || [];
+
+  useEffect(() => {
+    const current = child.find(
+      (item: DataMeter.ILayoutChild) => item.key == active,
+    );
+    if (current) setTitle(current.title);
+  }, [active]);
+
+  const getTitle = (title: string) => {
+    return (
+      <Popover
+        placement="topLeft"
+        content={<div style={{ maxWidth: '2rem' }}>{title}</div>}
+      >
+        {title}
+      </Popover>
+    );
+  };
+  var content;
+
+  switch (active) {
+    case 1:
+      if (fileList.length > 0) {
+        content = (
+          <>
+            <table
+              className={style.other}
+              style={{ width: 'calc(100% - 16px)' }}
+            >
+              <thead>
+                <tr>
+                  <th
+                    style={
+                      subModule == 0
+                        ? { width: '40%', paddingRight: '10px' }
+                        : { width: '50%', paddingRight: '10px' }
+                    }
+                  >
+                    文件名称
+                  </th>
+                  <th
+                    style={
+                      subModule == 0
+                        ? { width: '20%', paddingRight: '10px' }
+                        : { width: '50%', paddingRight: '10px' }
+                    }
+                  >
+                    日期
+                  </th>
+                  {subModule == 0 && (
+                    <th
+                      style={
+                        subModule == 0
+                          ? { width: '40%', paddingRight: '10px' }
+                          : { width: '50%', paddingRight: '10px' }
+                      }
+                    >
+                      项目名称
+                    </th>
+                  )}
+                </tr>
+              </thead>
+            </table>
+            <div
+              style={{
+                overflowY: 'scroll',
+                height: 'calc(100% - 24px)',
+              }}
+            >
+              <table className={style.other}>
+                <tbody>
+                  {fileList.map((item: any) => (
+                    <>
+                      <tr className={style.messageContent} key={item.Id}>
+                        <td
+                          style={
+                            subModule == 0
+                              ? { width: '40%', paddingRight: '10px' }
+                              : { width: '50%', paddingRight: '10px' }
+                          }
+                          className={style.alarmTitle}
+                        >
+                          {getTitle(item.Name)}
+                        </td>
+                        <td
+                          style={
+                            subModule == 0
+                              ? { width: '20%', paddingRight: '10px' }
+                              : { width: '50%', paddingRight: '10px' }
+                          }
+                        >
+                          {moment(item.CreatedTime).format('YYYY-MM-DD')}
+                        </td>
+                        {subModule == 0 && (
+                          <td
+                            style={{ width: '40%', paddingRight: '10px' }}
+                            className={style.alarmTitle}
+                          >
+                            {getTitle(getProject(item.ProjectId)?.Name || '无')}
+                          </td>
+                        )}
+                      </tr>
+                    </>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </>
+        );
+      } else {
+        content = <Empty />;
+      }
+      break;
+    case 2:
+      if (dailyList.length > 0) {
+        content = (
+          <>
+            <table
+              className={style.other}
+              style={{ width: 'calc(100% - 16px)' }}
+            >
+              <thead>
+                <tr>
+                  <th
+                    style={
+                      subModule == 0
+                        ? { width: '25%', paddingRight: '10px' }
+                        : { width: '50%', paddingRight: '10px' }
+                    }
+                  >
+                    创建人
+                  </th>
+                  <th
+                    style={
+                      subModule == 0
+                        ? { width: '20%', paddingRight: '10px' }
+                        : { width: '50%', paddingRight: '10px' }
+                    }
+                  >
+                    日期
+                  </th>
+                  {subModule == 0 && (
+                    <th style={{ width: '55%', paddingRight: '10px' }}>
+                      项目名称
+                    </th>
+                  )}
+                </tr>
+              </thead>
+            </table>
+            <div
+              style={{
+                overflowY: 'scroll',
+                height: 'calc(100% - 24px)',
+              }}
+            >
+              <table className={style.other}>
+                <tbody>
+                  {dailyList.map((item: any) => (
+                    <>
+                      <tr className={style.messageContent} key={item.id}>
+                        <td
+                          style={
+                            subModule == 0
+                              ? { width: '25%', paddingRight: '10px' }
+                              : { width: '50%', paddingRight: '10px' }
+                          }
+                          className={style.alarmTitle}
+                        >
+                          {getTitle(item.CreatorUser.CName)}
+                        </td>
+                        <td
+                          style={
+                            subModule == 0
+                              ? { width: '20%', paddingRight: '10px' }
+                              : { width: '50%', paddingRight: '10px' }
+                          }
+                        >
+                          {moment(item.ReportDate).format('YYYY-MM-DD')}
+                        </td>
+                        {subModule == 0 && (
+                          <td
+                            style={{ width: '55%', paddingRight: '10px' }}
+                            className={style.alarmTitle}
+                          >
+                            {getTitle(getProject(item.ProjectId)?.Name || '无')}
+                          </td>
+                        )}
+                      </tr>
+                    </>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </>
+        );
+      } else {
+        content = <Empty />;
+      }
+      break;
+  }
+
+  return (
+    <div className={style.modelBox}>
+      <ChartBoxTitle
+        title={title}
+        showTabs={showTabs}
+        setShowTabs={setShowTabs}
+        width={layout.w}
+      ></ChartBoxTitle>
+      {showTabs && (
+        <ul className={style.tabsList}>
+          {(child || [])
+            .filter((item: DataMeter.ILayoutChild) => item.show)
+            .map((item: DataMeter.ILayoutChild, index: number) => (
+              <li
+                key={index}
+                className={`${active == item.key ? style.active : ''}`}
+                onClick={() => {
+                  setActive(item.key);
+                  setSelfActive(item.key);
+                }}
+              >
+                {item.title}
+              </li>
+            ))}
+        </ul>
+      )}
+      <div
+        style={{
+          paddingTop: '10px',
+          paddingLeft: '14px',
+          flex: 1,
+          height: 0,
+        }}
+      >
+        {content}
+      </div>
+    </div>
+  );
+}
+
+export default Other;

+ 369 - 0
src/Project/pages/DataMeter/Model/PlanManagement.tsx

@@ -0,0 +1,369 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Empty } from 'antd';
+import * as echarts from 'echarts';
+import style from './index.less';
+import moment from 'moment';
+import { ChartBoxTitle } from './ChartBox';
+import { useRequest } from '@umijs/max';
+import {
+  getProjectActive,
+  getProjectPlanProgress,
+  getProjectRealProgress,
+} from '@/Project/services/DataMeter';
+
+function PlanManagement(props: DataMeter.IModelsProps) {
+  const {
+    child,
+    setActive,
+    layout,
+    projectId,
+    layout: { h, w },
+  } = props;
+  const [active, setSelfActive] = useState(
+    layout.active || child.find((item) => item.show)?.key,
+  );
+  const [chart, setChart] = useState<echarts.ECharts>();
+  const [showTabs, setShowTabs] = useState(false);
+  const [title, setTitle] = useState('');
+  const chartEle = useRef<HTMLDivElement | null>(null);
+
+  const projectActiveRequest = useRequest(getProjectActive, {
+    defaultParams: [{ projectId, type: 0 }],
+  });
+  const planProgressRequest = useRequest(getProjectPlanProgress, {
+    defaultParams: [projectId],
+  });
+  const realProgressRequest = useRequest(getProjectRealProgress, {
+    defaultParams: [
+      {
+        projectId,
+        ptime: moment().format('YYYY-MM-DD'),
+      },
+    ],
+  });
+
+  const progress: any = planProgressRequest.data?.list || [];
+  const realProgress: any = realProgressRequest.data || {};
+  const projectActive: any = projectActiveRequest.data?.list || [];
+
+  useEffect(() => {
+    if (chart) {
+      chart.resize();
+    }
+  }, [h, w, showTabs]);
+
+  useEffect(() => {
+    if (!chartEle.current) return;
+    let chart = echarts.init(chartEle.current);
+    let option = getOptions({});
+    chart.setOption(option);
+    setChart(chart);
+  }, [projectId]);
+
+  useEffect(() => {
+    const current = child.find((item) => item.key == active);
+    if (current) setTitle(current.title);
+  }, [active]);
+
+  useEffect(() => {
+    let xData, data1, data2;
+    let planTime: moment.Moment[] = [];
+    let realTime: moment.Moment[] = [];
+    if (!progress || !chart || !active || active == 7 || !realProgress) return;
+    switch (active) {
+      case 1:
+        data1 = [(progress.ArriveProgress * 100).toFixed(2)];
+        data2 = [realProgress[15]];
+        xData = ['到货'];
+        break;
+      case 2:
+        data1 = [(progress.ConstructionProgress * 100).toFixed(2)];
+        data2 = [realProgress[3]];
+        xData = ['施工'];
+        break;
+      case 3:
+        data1 = [(progress.SingleDebugProgress * 100).toFixed(2)];
+        data2 = [realProgress[12]];
+        xData = ['单机调试'];
+        break;
+      case 4:
+        planTime = [
+          moment(progress.MachineSystemTestPlanStartDate),
+          moment(progress.MachineSystemTestPlanEndDate),
+        ];
+        realTime = [
+          moment(progress.MachineSystemTestRealStartDate),
+          moment(progress.MachineSystemTestRealEndDate),
+        ];
+        break;
+      case 5:
+        planTime = [
+          moment(progress.TrialRunPlanStartDate),
+          moment(progress.TrialRunPlanEndDate),
+        ];
+        realTime = [
+          moment(progress.TrialRunRealStartDate),
+          moment(progress.TrialRunRealEndDate),
+        ];
+        break;
+    }
+
+    if (active <= 3) {
+      chart.setOption(getOptions({ xData, data1, data2 }), true);
+    } else if (active <= 5) {
+      var diffDay = planTime[0].diff(realTime[0], 'days');
+      if (diffDay >= 0) {
+        data1 = [diffDay, 0];
+      } else {
+        data1 = [0, diffDay * -1];
+      }
+      data2 = [
+        planTime[1].diff(planTime[0], 'days'),
+        realTime[1].diff(realTime[0], 'days'),
+      ];
+      chart.setOption(getOptions2({ data1, data2 }), true);
+    }
+  }, [active, progress, realProgress, chart]);
+
+  var content;
+  if (active == 7) {
+    if (projectActive.length > 0) {
+      content = (
+        <ul className={style.messageList}>
+          {projectActive.map((item: any) => (
+            <li key={item.id}>
+              <div className={style.messageTitle}>
+                {moment(item.c_time).format('YYYY-MM-DD')}
+              </div>
+              <div className={style.messageContent}>{item.content}</div>
+            </li>
+          ))}
+        </ul>
+      );
+    } else {
+      content = <Empty />;
+    }
+  } else {
+    content = '';
+  }
+  return (
+    <div className={style.modelBox}>
+      {/* <ModelTitle active={showTabs} setActive={setShowTabs}>
+        {title}
+      </ModelTitle> */}
+      <ChartBoxTitle
+        title={title}
+        showTabs={showTabs}
+        setShowTabs={setShowTabs}
+        width={layout.w}
+      />
+      {showTabs && (
+        <ul className={style.tabsList}>
+          {(child || [])
+            .filter((item) => item.show)
+            .map((item) => (
+              <li
+                key={item.key}
+                className={`${active == item.key ? style.active : ''}`}
+                onClick={() => {
+                  setActive(item.key);
+                  setSelfActive(item.key);
+                }}
+              >
+                {item.title}
+              </li>
+            ))}
+        </ul>
+      )}
+      <div className={style.modelContent}>
+        {content}
+        <div
+          className={style.chartBox}
+          style={{ height: '100%', display: active == 7 ? 'none' : 'flex' }}
+        >
+          <div ref={chartEle} className={style.planChart}></div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+const getOptions = ({ xData, data1, data2 }: any) => {
+  return {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+    },
+    legend: {
+      show: true,
+      top: 0,
+      right: 0,
+      textStyle: {
+        color: 'white',
+      },
+      data: ['计划', '实际'],
+    },
+    color: ['#7A96BF', '#284D86'],
+    xAxis: {
+      type: 'category',
+      axisLine: {
+        lineStyle: {
+          color: '#fff',
+        },
+      },
+      axisLabel: {
+        color: '#fff',
+        fontSize: 12,
+      },
+      axisTick: {
+        show: false,
+      },
+      data: xData,
+    },
+    yAxis: {
+      type: 'value',
+      min: 0,
+      max: 100,
+      axisLine: {
+        lineStyle: {
+          color: '#fff',
+        },
+      },
+      axisLabel: {
+        formatter: '{value}%',
+        color: '#fff',
+        fontSize: 12,
+      },
+      splitLine: {
+        lineStyle: {
+          color: '#5E6B86',
+        },
+      },
+    },
+    grid: {
+      top: 34,
+      left: '2%',
+      right: '2%',
+      bottom: 10,
+      containLabel: true,
+    },
+    series: [
+      {
+        name: '计划',
+        type: 'bar',
+        barCategoryGap: '20%',
+        label: {
+          normal: {
+            show: true,
+            position: 'top',
+            fontSize: 11,
+            formatter: function (params: any) {
+              if (params.value) {
+                return params.value + '%';
+              } else {
+                return 0 + '%';
+              }
+            },
+          },
+        },
+        data: data1 || [],
+      },
+      {
+        name: '实际',
+        type: 'bar',
+        label: {
+          normal: {
+            show: true,
+            position: 'top',
+            fontSize: 11,
+            formatter: function (params: any) {
+              if (params.value) {
+                return params.value + '%';
+              } else {
+                return 0;
+              }
+            },
+          },
+        },
+        data: data2 || [],
+      },
+    ],
+  };
+};
+const getOptions2 = ({ data1, data2 }: any) => {
+  return {
+    color: ['#7A96BF', '#5072A0'],
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        // 坐标轴指示器,坐标轴触发有效
+        type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
+      },
+      formatter: function (params: any) {
+        var tar = params[1];
+        return tar.name + '<br/>' + (tar.value || 0) + '天';
+      },
+    },
+    grid: {
+      top: 34,
+      left: '2%',
+      right: '2%',
+      bottom: 10,
+      containLabel: true,
+    },
+    xAxis: {
+      type: 'value',
+      splitLine: { show: false },
+      axisLabel: { show: false },
+      axisLine: {
+        lineStyle: {
+          color: '#fff',
+        },
+      },
+    },
+    yAxis: {
+      type: 'category',
+      splitLine: { show: false },
+      data: ['计划时间', '实际时间'],
+      axisLine: {
+        lineStyle: {
+          color: '#fff',
+        },
+      },
+      axisLabel: {
+        color: '#fff',
+        fontSize: 12,
+      },
+    },
+    series: [
+      {
+        name: '辅助',
+        type: 'bar',
+        stack: '总量',
+        itemStyle: {
+          barBorderColor: 'rgba(0,0,0,0)',
+          color: 'rgba(0,0,0,0)',
+        },
+        emphasis: {
+          itemStyle: {
+            barBorderColor: 'rgba(0,0,0,0)',
+            color: 'rgba(0,0,0,0)',
+          },
+        },
+        data: data1,
+      },
+      {
+        name: '时间',
+        type: 'bar',
+        stack: '总量',
+        label: {
+          show: false,
+        },
+        data: data2,
+      },
+    ],
+  };
+};
+export default PlanManagement;

+ 284 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/api.js

@@ -0,0 +1,284 @@
+import request from 'superagent';
+import axios from 'axios';
+import JSZip from 'jszip';
+import FileSaver from 'file-saver';
+import moment from 'moment';
+import { message } from 'antd';
+
+// axios.interceptors.request.use(config => {
+//   config.withCredentials = true;
+//   return config;
+// });
+
+import { normalizeResource } from './utils/common';
+const number = new Date().getTime();
+
+const getFile = url => {
+  const down = url.replace('https://water-service.oss-cn-hangzhou.aliyuncs.com/', '');
+  let str = '';
+  if (process.env.APP_TYPE === 'site' || process.env.NODE_ENV !== 'production') {
+    str = `v1/${down}`;
+  } else {
+    str = url;
+  }
+
+  return new Promise((resolve, reject) => {
+    axios
+      .get(str, {
+        responseType: 'blob',
+        // header: {
+        //   "Access-Control-Allow-Origin": "*",
+        //   "Access-Control-Allow-Headers": "X-Requested-With,Content-Type",
+        //   "Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS",
+        // },
+      })
+      .then(data => {
+        resolve(data.data);
+      })
+      .catch(error => {
+        reject(error.toString());
+      });
+  });
+};
+
+/**
+ * hasSignedIn
+ *
+ * @returns {boolean}
+ */
+function hasSignedIn() {
+  return true;
+}
+
+/**
+ * Init API
+ *
+ * @returns {Promise<{apiInitialized: boolean, apiSignedIn: boolean}>}
+ */
+function init() {
+  return {
+    apiInitialized: true,
+    apiSignedIn: true,
+  };
+}
+
+async function getCapabilitiesForResource(options, resource) {
+  return resource.capabilities || [];
+}
+
+async function getResourceById(options, id) {
+  const route = `${options.apiRoot}/files/${id}?JWT-TOKEN=${options.token}&time=${number}`;
+  const method = 'GET';
+  const response = await request(method, route);
+  // const { body } = response;
+  // console.log(body)
+  // 将根目录名称修改为 '/'
+  // if (body.ancestors && body.ancestors.length != 0) {
+  //   body.ancestors[0].name = '/'
+  // } else {
+  //   body.name = '/'
+  // }
+
+  return normalizeResource(response.body);
+}
+
+async function getChildrenForId(options, { id, sortBy = 'name', sortDirection = 'ASC', depId }) {
+  const tempDepId = depId || localStorage.getItem('depId');
+  const route = `${options.apiRoot}/files/${id}/children?depId=${tempDepId}&orderBy=${sortBy}&orderDirection=${sortDirection}&JWT-TOKEN=${options.token}&searchContent=${options.searchContent}&time=${number}`;
+  const method = 'GET';
+  const response = await request(method, route);
+  return (response.body.items || []).map(normalizeResource);
+}
+
+async function getChildrenPath(data, parentPath, apiOptions) {
+  var pathArr = [];
+  for (var i = 0; i < data.length; i++) {
+    let item = data[i];
+    if (item.type == 'dir') {
+      // 获取目录子级
+      const res = await getChildrenForId(apiOptions, { id: item.id });
+      if (res && res.length > 0) {
+        // 获取文件列表
+        pathArr = pathArr.concat(
+          await getChildrenPath(res, `${parentPath}/${item.name}`, apiOptions)
+        );
+      }
+    } else {
+      pathArr.push(`${parentPath}/${item.name}`);
+    }
+  }
+  console.log(pathArr);
+  return pathArr;
+}
+
+async function getParentsForId(options, id, result = []) {
+  if (!id) {
+    return result;
+  }
+
+  const resource = await getResourceById(options, id);
+  if (resource && resource.ancestors) {
+    return resource.ancestors;
+  }
+  return result;
+}
+
+async function getBaseResource(options) {
+  const route = `${options.apiRoot}/files?time=${number}`;
+  const response = await request.get(route);
+  return normalizeResource(response.body);
+}
+
+async function getIdForPartPath(options, currId, pathArr) {
+  const resourceChildren = await getChildrenForId(options, { id: currId });
+  for (let i = 0; i < resourceChildren.length; i++) {
+    const resource = resourceChildren[i];
+    if (resource.name === pathArr[0]) {
+      if (pathArr.length === 1) {
+        return resource.id;
+      } else {
+        return getIdForPartPath(options, resource.id, pathArr.slice(1));
+      }
+    }
+  }
+
+  return null;
+}
+
+async function getIdForPath(options, path) {
+  const resource = await getBaseResource(options);
+  const pathArr = path.split('/');
+  if (pathArr.length === 0 || pathArr.length === 1 || pathArr[0] !== '') {
+    return null;
+  }
+
+  if (pathArr.length === 2 && pathArr[1] === '') {
+    return resource.id;
+  }
+
+  return getIdForPartPath(options, resource.id, pathArr.slice(1));
+}
+
+async function getParentIdForResource(options, resource) {
+  return resource.parentId;
+}
+
+async function uploadFileToId({ apiOptions, parentId, file, onProgress }) {
+  const depId = localStorage.getItem('depId');
+  const route = `${apiOptions.apiRoot}/files?depId=${depId}&JWT-TOKEN=${apiOptions.token}&time=${number}`;
+  return request
+    .post(route)
+    .field('type', 'file')
+    .field('parentId', parentId)
+    .attach('files', file.file, file.name)
+    .on('progress', event => {
+      onProgress(event.percent);
+    });
+}
+
+async function downloadResources({ apiOptions, resources, onProgress }) {
+  // const downloadUrl = resources.reduce(
+  //   (url, resource, num) => url + (num === 0 ? '' : '&') + `items=${resource.id}`,
+  //   `${apiOptions.apiMultipleRoot}/multi/download?`
+  // );
+  let str = '';
+  let con = '';
+  resources.map((item, index) => {
+    str += `${item.id},`;
+  });
+  con = str.substr(0, str.length - 1);
+  const downloadUrl = `${apiOptions.apiMultipleRoot}/multi/download?items=${con}`;
+  const res = await request.get(`${downloadUrl}&JWT-TOKEN=${apiOptions.token}`);
+  const zip = new JSZip();
+  const cache = {};
+  const promises = [];
+  if (res.body && res.body.length > 0) {
+  }
+  res.body.forEach(item => {
+    const promise = getFile(item).then(data => {
+      const arr_name = item.split('/'); // 下载文件, 并存成ArrayBuffer对象
+      const file_name = arr_name[arr_name.length - 1]; // 获取文件名
+      zip.file(file_name, data, { binary: true }); // 逐个添加文件
+      cache[file_name] = data;
+    });
+    promises.push(promise);
+  });
+
+  Promise.all(promises).then(() => {
+    zip.generateAsync({ type: 'blob' }).then(content => {
+      // 生成二进制流
+      // FileSaver.saveAs(blob, name);
+      const DateTime = moment().format('YYYY-MM-DD-HH-mm');
+      FileSaver.saveAs(content, `文件下载${DateTime}.zip`); // 利用file-saver保存文件  自定义文件名
+    });
+  });
+  // const downloadUrl = resources.map(item =>)
+  // const res = await request.get(`${downloadUrl}&JWT-TOKEN=${apiOptions.token}`).
+  //   responseType('blob').
+  //   on('progress', event => {
+  //     onProgress(event.percent);
+  //   });
+  //
+  // return res.body;
+}
+
+async function createFolder(options, parentId, folderName) {
+  const route = `${options.apiRoot}/files?JWT-TOKEN=${options.token}&time=${number}`;
+  const method = 'POST';
+  const params = {
+    parentId,
+    name: folderName,
+    type: 'dir',
+  };
+  return request(method, route).send(params);
+}
+
+function getResourceName(apiOptions, resource) {
+  return resource.name;
+}
+
+async function renameResource(options, id, newName) {
+  const route = `${options.apiRoot}/files/${id}?JWT-TOKEN=${options.token}&time=${number}`;
+  const method = 'PATCH';
+  return request(method, route)
+    .type('application/json')
+    .send({ name: newName })
+    .then(res => {
+      var {
+        body: { msg },
+      } = res;
+      if (msg) {
+        message.error(msg);
+      }
+      return res;
+    });
+}
+
+async function removeResource(options, resource) {
+  const route = `${options.apiRoot}/files/${resource.id}?JWT-TOKEN=${options.token}&time=${number}`;
+  const method = 'DELETE';
+  return request(method, route);
+}
+
+async function removeResources(options, selectedResources) {
+  return Promise.all(selectedResources.map(resource => removeResource(options, resource)));
+}
+
+export default {
+  init,
+  hasSignedIn,
+  getBaseResource,
+  getIdForPath,
+  getResourceById,
+  getCapabilitiesForResource,
+  getChildrenForId,
+  getChildrenPath,
+  getParentsForId,
+  getParentIdForResource,
+  getResourceName,
+  createFolder,
+  downloadResources,
+  renameResource,
+  removeResources,
+  uploadFileToId,
+};

+ 2 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/apiOptions.js

@@ -0,0 +1,2 @@
+export default {
+};

+ 92 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/create-folder.js

@@ -0,0 +1,92 @@
+import api from '../api';
+import sanitizeFilename from 'sanitize-filename';
+import onFailError from '../utils/onFailError';
+import icons from '../icons-svg';
+import getMess from '../translations';
+
+const label = 'createFolder';
+
+function handler(apiOptions, actions) {
+  const {
+    showDialog,
+    hideDialog,
+    navigateToDir,
+    updateNotifications,
+    getResource,
+    getNotifications
+  } = actions;
+
+  const getMessage = getMess.bind(null, apiOptions.locale);
+
+  const rawDialogElement = {
+    elementType: 'SetNameDialog',
+    elementProps: {
+      onHide: hideDialog,
+      onSubmit: async (folderName) => {
+        const resource = getResource();
+        try {
+          const resourceChildren = await api.getChildrenForId(apiOptions, { id: resource.id });
+          const alreadyExists = resourceChildren.some(({ name }) => name === folderName);
+
+          if (alreadyExists) {
+            return getMessage('fileExist', { name: folderName });
+          } else {
+            hideDialog();
+            const result = await api.createFolder(apiOptions, resource.id, folderName);
+            navigateToDir(resource.id, result.body.id, false);
+          }
+          return null
+        } catch (err) {
+          hideDialog();
+          onFailError({
+            getNotifications,
+            label: getMessage(label),
+            notificationId: label,
+            updateNotifications
+          });
+          console.log(err);
+          return null
+        }
+      },
+      onValidate: async (folderName) => {
+        if (!folderName) {
+          return getMessage('emptyName');
+        } else if (folderName === 'CON') {
+          return getMessage('doNotRespectBill');
+        } else if (folderName.length >= 255) {
+          return getMessage('tooLongFolderName');
+        } else if (folderName.trim() !== sanitizeFilename(folderName.trim())) {
+          return getMessage('folderNameNotAllowedCharacters');
+        }
+        return null;
+      },
+      inputLabelText: getMessage('folderName'),
+      headerText: getMessage('createFolder'),
+      submitButtonText: getMessage('create'),
+      cancelButtonText: getMessage('cancel')
+    }
+  };
+
+  showDialog(rawDialogElement);
+}
+
+export default (apiOptions, actions) => {
+  const localeLabel = getMess(apiOptions.locale, label);
+  const { getResource } = actions;
+  return {
+    id: label,
+    icon: { svg: icons.createNewFolder },
+    label: localeLabel,
+    shouldBeAvailable: (apiOptions) => {
+      const resource = getResource();
+
+      if (!resource || !resource.capabilities) {
+        return false;
+      }
+
+      return resource.capabilities.canAddChildren;
+    },
+    availableInContexts: ['files-view', 'new-button'],
+    handler: () => handler(apiOptions, actions)
+  };
+}

+ 78 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/delete-resource.js

@@ -0,0 +1,78 @@
+import api from '../api';
+import onFailError from '../utils/onFailError';
+import icons from '../icons-svg';
+import getMess from '../translations';
+
+const label = 'remove';
+
+function handler(apiOptions, actions) {
+  const {
+    showDialog,
+    hideDialog,
+    navigateToDir,
+    updateNotifications,
+    getSelectedResources,
+    getResource,
+    getNotifications
+  } = actions;
+
+  const getMessage = getMess.bind(null, apiOptions.locale);
+
+  const selectedResources = getSelectedResources();
+
+  const dialogFilesText = selectedResources.length > 1 ?
+    `${selectedResources.length} ${getMessage('files')}` :
+    `"${selectedResources[0].name}"`;
+
+  const dialogNameText = getMessage('reallyRemove', { files: dialogFilesText });
+
+  const rawDialogElement = {
+    elementType: 'ConfirmDialog',
+    elementProps: {
+      onHide: hideDialog,
+      onSubmit: async () => {
+        hideDialog();
+        try {
+          await api.removeResources(apiOptions, selectedResources);
+          const resource = getResource();
+          navigateToDir(resource.id, null, false);
+        } catch (err) {
+          onFailError({
+            getNotifications,
+            label: getMessage(label),
+            notificationId: 'delete',
+            updateNotifications
+          });
+          console.log(err)
+        }
+      },
+      headerText: getMessage('remove'),
+      messageText: dialogNameText,
+      cancelButtonText: getMessage('cancel'),
+      submitButtonText: getMessage('confirm')
+    }
+  };
+
+  showDialog(rawDialogElement);
+}
+
+export default (apiOptions, actions) => {
+  const localeLabel = getMess(apiOptions.locale, label);
+  const { getSelectedResources } = actions;
+  return {
+    id: 'delete',
+    icon: { svg: icons.delete },
+    label: localeLabel,
+    shouldBeAvailable: (apiOptions) => {
+      const selectedResources = getSelectedResources();
+
+      if (!selectedResources.length) {
+        return false;
+      }
+
+      return selectedResources.every(resource => resource.capabilities.canDelete);
+    },
+    availableInContexts: ['row', 'toolbar'],
+    handler: () => handler(apiOptions, actions)
+  };
+}

+ 142 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/download.js

@@ -0,0 +1,142 @@
+import api from '../api';
+import notifUtils from '../utils/notifications';
+import { promptToSaveBlob } from '../utils/download';
+import onFailError from '../utils/onFailError';
+import nanoid from 'nanoid';
+import icons from '../icons-svg';
+import getMess from '../translations';
+
+const label = 'download';
+
+async function handler(apiOptions, actions) {
+  const {
+    updateNotifications,
+    getSelectedResources,
+    getNotifications
+  } = actions;
+
+  const getMessage = getMess.bind(null, apiOptions.locale);
+
+  const notificationId = label;
+  const notificationChildId = nanoid();
+
+  const onStart = ({ archiveName, quantity }) => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+
+    const childElement = {
+      elementType: 'NotificationProgressItem',
+      elementProps: {
+        title: getMessage('creatingName', { name: archiveName }),
+        progress: 0
+      }
+    };
+
+    const newChildren = notifUtils.addChild(
+      (notification && notification.children) || [], notificationChildId, childElement
+    );
+    const newNotification = {
+      title: quantity > 1 ? getMessage('zippingItems', { quantity }) : getMessage('zippingItem'),
+      children: newChildren
+    };
+
+    const newNotifications = notification ?
+      notifUtils.updateNotification(notifications, notificationId, newNotification) :
+      notifUtils.addNotification(notifications, notificationId, newNotification);
+
+    updateNotifications(newNotifications);
+  };
+
+  const onSuccess = _ => {
+    console.log(2);
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const notificationChildrenCount = notification.children.length;
+    let newNotifications;
+    console.log(3);
+
+    if (notificationChildrenCount > 1) {
+      console.log(4);
+      newNotifications = notifUtils.updateNotification(
+        notifications,
+        notificationId, {
+          children: notifUtils.removeChild(notification.children, notificationChildId)
+        }
+      );
+    } else {
+      console.log(5);
+      newNotifications = notifUtils.removeNotification(notifications, notificationId);
+    }
+    console.log(newNotifications);
+    updateNotifications(newNotifications);
+  };
+
+  const onProgress = (progress) => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const child = notifUtils.getChild(notification.children, notificationChildId);
+
+    const newChild = {
+      ...child,
+      element: {
+        ...child.element,
+        elementProps: {
+          ...child.element.elementProps,
+          progress
+        }
+      }
+    };
+    const newChildren = notifUtils.updateChild(notification.children, notificationChildId, newChild);
+    const newNotifications = notifUtils.updateNotification(notifications, notificationId, { children: newChildren });
+    updateNotifications(newNotifications);
+  };
+
+  try {
+    const resources = getSelectedResources();
+    console.log(resources);
+    const quantity = resources.length;
+    if (quantity === 1) {
+      const { id, name } = resources[0];
+      const downloadUrl = `${apiOptions.apiRoot}/download?items=${id}`;
+      // check if the file is available and trigger native browser saving prompt
+      // if server is down the error will be catched and trigger relevant notification
+      await api.getResourceById(apiOptions, id);
+      promptToSaveBlob({ name, downloadUrl });
+    } else {
+      console.log(2);
+      // multiple resources -> download as a single archive
+      const archiveName = apiOptions.archiveName || 'archive.zip';
+      onStart({ archiveName, quantity });
+      const content = await api.downloadResources({ resources, apiOptions, onProgress });
+      setTimeout(onSuccess, 1000);
+      // promptToSaveBlob({ content, name: archiveName })
+    }
+  } catch (err) {
+    onFailError({
+      getNotifications,
+      label: getMessage(label),
+      notificationId,
+      updateNotifications
+    });
+  }
+}
+
+export default (apiOptions, actions) => {
+  const localeLabel = getMess(apiOptions.locale, label);
+  const { getSelectedResources } = actions;
+  return {
+    id: label,
+    icon: { svg: icons.fileDownload },
+    label: localeLabel,
+    shouldBeAvailable: (apiOptions) => {
+      const selectedResources = getSelectedResources();
+      return (
+        selectedResources.length > 0 &&
+        !selectedResources.some(r => r.type === 'dir') &&
+        selectedResources.every(r => r.capabilities.canDownload)
+      );
+    },
+    availableInContexts: ['row', 'toolbar'],
+    handler: () => handler(apiOptions, actions)
+  };
+}

+ 41 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/index.js

@@ -0,0 +1,41 @@
+import createFolder from './create-folder';
+import deleteResource from './delete-resource';
+import download from './download';
+import see from './see';
+import upload from './upload';
+import rename from './rename';
+import sort from './sort';
+
+const capabilities = [
+  createFolder,
+  rename,
+  download,
+  see,
+  upload,
+  deleteResource,
+  sort
+];
+
+/**
+ * Actions' fields list:
+ *  showDialog,
+ *  hideDialog,
+ *  navigateToDir,
+ *  updateNotifications,
+ *  getSelection,
+ *  getSelectedResources,
+ *  getResource,
+ *  getResourceChildren,
+ *  getResourceLocation,
+ *  getNotifications,
+ *  getSortState
+ *
+ *  Called from FileNavigator (componentDidMount() and componentWillReceiveProps())
+ *
+ * @param apiOptions
+ * @param {object} actions
+ * @returns {array}
+ */
+export default (apiOptions, actions) => (
+  capabilities.map(capability => capability(apiOptions, actions))
+);

+ 92 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/rename.js

@@ -0,0 +1,92 @@
+import api from '../api';
+import sanitizeFilename from 'sanitize-filename';
+import onFailError from '../utils/onFailError';
+import icons from '../icons-svg';
+import getMess from '../translations';
+
+const label = 'rename';
+
+function handler(apiOptions, actions) {
+  const {
+    showDialog,
+    hideDialog,
+    navigateToDir,
+    updateNotifications,
+    getSelectedResources,
+    getResource,
+    getNotifications
+  } = actions;
+
+  const getMessage = getMess.bind(null, apiOptions.locale);
+  const localeLabel = getMessage(label);
+
+  const rawDialogElement = {
+    elementType: 'SetNameDialog',
+    elementProps: {
+      initialValue: getSelectedResources()[0].name,
+      onHide: hideDialog,
+      onSubmit: async (name) => {
+        const selectedResources = getSelectedResources();
+        try {
+          const resourceChildren = await api.getChildrenForId(
+            apiOptions, { id: selectedResources[0].parentId }
+          );
+          const alreadyExists = resourceChildren.some(o => o.name === name);
+          if (alreadyExists) {
+            return getMessage('fileExist', { name });
+          } else {
+            hideDialog();
+            const result = await api.renameResource(apiOptions, selectedResources[0].id, name);
+            const resource = getResource();
+            navigateToDir(resource.id, result.body.id, false);
+          }
+          return null;
+        } catch (err) {
+          hideDialog();
+          onFailError({
+            getNotifications,
+            label: localeLabel,
+            notificationId: label,
+            updateNotifications
+          });
+          console.log(err);
+          return null
+        }
+      },
+      onValidate: async (name) => {
+        if (!name) {
+          return getMessage('emptyName');
+        } else if (name.length >= 255) {
+          return getMessage('tooLongFolderName');
+        } else if (name.trim() !== sanitizeFilename(name.trim())) {
+          return getMessage('folderNameNotAllowedCharacters');
+        }
+        return null;
+      },
+      inputLabelText: getMessage('newName'),
+      headerText: getMessage('rename'),
+      submitButtonText: localeLabel,
+      cancelButtonText: getMessage('cancel')
+    }
+  };
+  showDialog(rawDialogElement);
+}
+
+export default (apiOptions, actions) => {
+  const localeLabel = getMess(apiOptions.locale, label);
+  const { getSelectedResources } = actions;
+  return {
+    id: label,
+    icon: { svg: icons.rename },
+    label: localeLabel,
+    shouldBeAvailable: (apiOptions) => {
+      const selectedResources = getSelectedResources();
+      return (
+        selectedResources.length === 1 &&
+        selectedResources.every(r => r.capabilities.canRename)
+      );
+    },
+    availableInContexts: ['row', 'toolbar'],
+    handler: () => handler(apiOptions, actions)
+  };
+}

+ 137 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/see.js

@@ -0,0 +1,137 @@
+import api from '../api';
+import notifUtils from '../utils/notifications';
+import { promptToSaveBlob } from '../utils/see';
+import onFailError from '../utils/onFailError';
+import nanoid from 'nanoid';
+import icons from '../icons-svg';
+import getMess from '../translations';
+
+const label = 'see';
+
+async function handler(apiOptions, actions) {
+  const {
+    updateNotifications,
+    getSelectedResources,
+    getNotifications
+  } = actions;
+
+  const getMessage = getMess.bind(null, apiOptions.locale);
+
+  const notificationId = label;
+  const notificationChildId = nanoid();
+
+  const onStart = ({ archiveName, quantity }) => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+
+    const childElement = {
+      elementType: 'NotificationProgressItem',
+      elementProps: {
+        title: getMessage('creatingName', { name: archiveName }),
+        progress: 0
+      }
+    };
+
+    const newChildren = notifUtils.addChild(
+      (notification && notification.children) || [], notificationChildId, childElement
+    );
+    const newNotification = {
+      title: quantity > 1 ? getMessage('zippingItems', { quantity }) : getMessage('zippingItem'),
+      children: newChildren
+    };
+
+    const newNotifications = notification ?
+      notifUtils.updateNotification(notifications, notificationId, newNotification) :
+      notifUtils.addNotification(notifications, notificationId, newNotification);
+
+    updateNotifications(newNotifications);
+  };
+
+  const onSuccess = _ => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const notificationChildrenCount = notification.children.length;
+    let newNotifications;
+
+    if (notificationChildrenCount > 1) {
+      newNotifications = notifUtils.updateNotification(
+        notifications,
+        notificationId, {
+          children: notifUtils.removeChild(notification.children, notificationChildId)
+        }
+      );
+    } else {
+      newNotifications = notifUtils.removeNotification(notifications, notificationId);
+    }
+    updateNotifications(newNotifications);
+  };
+
+  const onProgress = (progress) => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const child = notifUtils.getChild(notification.children, notificationChildId);
+
+    const newChild = {
+      ...child,
+      element: {
+        ...child.element,
+        elementProps: {
+          ...child.element.elementProps,
+          progress
+        }
+      }
+    };
+    const newChildren = notifUtils.updateChild(notification.children, notificationChildId, newChild);
+    const newNotifications = notifUtils.updateNotification(notifications, notificationId, { children: newChildren });
+    updateNotifications(newNotifications);
+  };
+
+  try {
+    const resources = getSelectedResources();
+    const quantity = resources.length;
+    if (quantity === 1) {
+      const { id, name } = resources[0];
+      const downloadUrl = `${apiOptions.apiRoot}/download?items=${id}`;
+      // check if the file is available and trigger native browser saving prompt
+      // if server is down the error will be catched and trigger relevant notification
+      await api.getResourceById(apiOptions, id);
+      promptToSaveBlob({ name, downloadUrl });
+    } else {
+      // multiple resources -> download as a single archive
+      const archiveName = apiOptions.archiveName || 'archive.zip';
+      onStart({ archiveName, quantity });
+      const content = await api.downloadResources({ resources, apiOptions, onProgress });
+      setTimeout(onSuccess, 1000);
+      promptToSaveBlob({ content, name: archiveName })
+    }
+  } catch (err) {
+    onFailError({
+      getNotifications,
+      label: getMessage(label),
+      notificationId,
+      updateNotifications
+    });
+    console.log(err)
+  }
+}
+
+export default (apiOptions, actions) => {
+  const localeLabel = getMess(apiOptions.locale, label);
+  const { getSelectedResources } = actions;
+  return {
+    id: label,
+    icon: { svg: icons.book },
+    label: localeLabel,
+    shouldBeAvailable: (apiOptions) => {
+      const selectedResources = getSelectedResources();
+
+      return (
+        selectedResources.length > 0 &&
+        !selectedResources.some(r => r.type === 'dir') &&
+        selectedResources.every(r => r.capabilities.canDownload)
+      );
+    },
+    availableInContexts: ['row', 'toolbar'],
+    handler: () => handler(apiOptions, actions)
+  };
+}

+ 28 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/sort.js

@@ -0,0 +1,28 @@
+import api from '../api';
+import onFailError from '../utils/onFailError';
+
+export default (apiOptions, actions) => {
+  const {
+    updateNotifications,
+    getResource,
+    getNotifications,
+    getSortState // eslint-disable-line no-unused-vars
+  } = actions;
+  return ({
+    id: 'sort',
+    shouldBeAvailable: () => true,
+    handler: async ({ sortBy, sortDirection }) => {
+      const id = getResource().id;
+      try {
+        return api.getChildrenForId(apiOptions, { id, sortBy, sortDirection });
+      } catch (err) {
+        onFailError({
+          getNotifications,
+          notificationId: 'rename',
+          updateNotifications
+        });
+        return null
+      }
+    }
+  });
+}

+ 126 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/capabilities/upload.js

@@ -0,0 +1,126 @@
+import api from '../api';
+import notifUtils from '../utils/notifications';
+import { getIcon } from '../icons';
+import nanoid from 'nanoid';
+import onFailError from '../utils/onFailError';
+import { readLocalFile } from '../utils/upload';
+import icons from '../icons-svg';
+import getMess from '../translations';
+import { normalizeResource } from '../utils/common';
+
+const label = 'upload';
+
+async function handler(apiOptions, actions) {
+  const {
+    navigateToDir,
+    updateNotifications,
+    getResource,
+    getNotifications
+  } = actions;
+
+  const getMessage = getMess.bind(null, apiOptions.locale);
+
+  const notificationId = label;
+  const notificationChildId = nanoid();
+  const prevResourceId = getResource().id;
+
+  const onStart = ({ name, size }) => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const childElement = {
+      elementType: 'NotificationProgressItem',
+      elementProps: {
+        title: name,
+        progress: 0,
+        icon: getIcon({ name })
+      }
+    };
+
+    const newChildren =
+      notifUtils.addChild((notification && notification.children) || [], notificationChildId, childElement);
+    const newNotification = {
+      title: newChildren.length > 1 ?
+        getMessage('uploadingItems', { quantity: newChildren.length }) :
+        getMessage('uploadingItem'),
+      children: newChildren
+    };
+
+    const newNotifications = notification ?
+      notifUtils.updateNotification(notifications, notificationId, newNotification) :
+      notifUtils.addNotification(notifications, notificationId, newNotification);
+
+    updateNotifications(newNotifications);
+  };
+
+  const onProgress = progress => {
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const child = notifUtils.getChild(notification.children, notificationChildId);
+    const newChild = {
+      ...child,
+      element: {
+        ...child.element,
+        elementProps: {
+          ...child.element.elementProps,
+          progress
+        }
+      }
+    };
+    const newChildren = notifUtils.updateChild(notification.children, notificationChildId, newChild);
+    const newNotifications = notifUtils.updateNotification(notifications, notificationId, { children: newChildren });
+    updateNotifications(newNotifications);
+  };
+
+  const resource = getResource();
+  try {
+    const file = await readLocalFile(true);
+    onStart({ name: file.name, size: file.file.size });
+    const response = await api.uploadFileToId({ apiOptions, parentId: resource.id, file, onProgress });
+    const newResource = normalizeResource(response.body[0]);
+    const notifications = getNotifications();
+    const notification = notifUtils.getNotification(notifications, notificationId);
+    const notificationChildrenCount = notification.children.length;
+    let newNotifications;
+    if (notificationChildrenCount > 1) {
+      newNotifications = notifUtils.updateNotification(
+        notifications,
+        notificationId, {
+          children: notifUtils.removeChild(notification.children, notificationChildId)
+        }
+      );
+    } else {
+      newNotifications = notifUtils.removeNotification(notifications, notificationId);
+    }
+    updateNotifications(newNotifications);
+    if (prevResourceId === resource.id) {
+      navigateToDir(resource.id, newResource.id, false);
+    }
+  } catch (err) {
+    onFailError({
+      getNotifications,
+      label: getMessage(label),
+      notificationId,
+      updateNotifications
+    });
+    console.log(err)
+  }
+}
+
+export default (apiOptions, actions) => {
+  const localeLabel = getMess(apiOptions.locale, label);
+  const { getResource } = actions;
+  return {
+    id: label,
+    icon: { svg: icons.fileUpload },
+    label: localeLabel,
+    shouldBeAvailable: (apiOptions) => {
+      const resource = getResource();
+      if (!resource || !resource.capabilities) {
+        return false
+      }
+      return resource.capabilities.canAddChildren
+    },
+    availableInContexts: ['files-view', 'new-button'],
+    handler: () => handler(apiOptions, actions)
+  };
+}

+ 21 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/icons-svg.js

@@ -0,0 +1,21 @@
+// Icons preview can be found at
+// https://github.com/OpusCapita/react-svg or
+// https://github.com/OpusCapita/svg-icons
+
+/* eslint-disable max-len */
+export default {
+  fileDownload: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M38 18h-8V6H18v12h-8l14 14 14-14zM10 36v4h28v-4H10z"/></svg>`,
+  fileUpload: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M18 32h12V20h8L24 6 10 20h8zm-8 4h28v4H10z"/></svg>`,
+  createNewFolder: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M40 12H24l-4-4H8c-2.21 0-3.98 1.79-3.98 4L4 36c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V16c0-2.21-1.79-4-4-4zm-2 16h-6v6h-4v-6h-6v-4h6v-6h4v6h6v4z"/></svg>`,
+  delete: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M12 38c0 2.21 1.79 4 4 4h16c2.21 0 4-1.79 4-4V14H12v24zM38 8h-7l-2-2H19l-2 2h-7v4h28V8z"/></svg>`,
+  rename: `<svg class="a-s-fa-Ha-pa" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" focusable="false" fill="rgba(0, 0, 0, 0.72)"><path d="M0 0h24v24H0z" fill="none"></path><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM6 17v-2.47l7.88-7.88c.2-.2.51-.2.71 0l1.77 1.77c.2.2.2.51 0 .71L8.47 17H6zm12 0h-7.5l2-2H18v2z"></path></svg>`,
+  folder: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M20 8H8c-2.21 0-3.98 1.79-3.98 4L4 36c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V16c0-2.21-1.79-4-4-4H24l-4-4z"/></svg>`,
+  volumeUp: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M6 18v12h8l10 10V8L14 18H6zm27 6c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zM28 6.46v4.13c5.78 1.72 10 7.07 10 13.41s-4.22 11.69-10 13.41v4.13c8.01-1.82 14-8.97 14-17.54S36.01 8.28 28 6.46z"/></svg>`,
+  image: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M42 38V10c0-2.21-1.79-4-4-4H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4zM17 27l5 6.01L29 24l9 12H10l7-9z"/></svg>`,
+  ondemandVideo: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M42 6H6c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h10v4h16v-4h10c2.21 0 3.98-1.79 3.98-4L46 10c0-2.21-1.79-4-4-4zm0 28H6V10h36v24zM32 22l-14 8V14z"/></svg>`,
+  archive: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M41.09 10.45l-2.77-3.36C37.76 6.43 36.93 6 36 6H12c-.93 0-1.76.43-2.31 1.09l-2.77 3.36C6.34 11.15 6 12.03 6 13v25c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V13c0-.97-.34-1.85-.91-2.55zM24 35L13 24h7v-4h8v4h7L24 35zM10.25 10l1.63-2h24l1.87 2h-27.5z"/></svg>`,
+  book: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M36 4H12C9.79 4 8 5.79 8 8v32c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4V8c0-2.21-1.79-4-4-4zM12 8h10v16l-5-3-5 3V8z"/></svg>`,
+  insertDriveFile: `<svg  xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" focusable="false"><path d="M12 4C9.79 4 8.02 5.79 8.02 8L8 40c0 2.21 1.77 4 3.98 4H36c2.21 0 4-1.79 4-4V16L28 4H12zm14 14V7l11 11H26z"/></svg>`,
+  warning: ``
+};
+/* eslint-enable */

+ 39 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/icons.js

@@ -0,0 +1,39 @@
+import icons from './icons-svg';
+
+const dirIcon = icons.folder;
+const soundFileIcon = icons.volumeUp;
+const pictureFileIcon = icons.image;
+const videoFileIcon = icons.ondemandVideo;
+const archiveFileIcon = icons.archive;
+const booksFileIcon = icons.book;
+const unknownFileIcon = icons.insertDriveFile;
+
+const defaultFillColor = '#424242';
+const soundFilesExtensions = ['aac', 'aiff', 'flac', 'm4a', 'ogg', 'mp3', 'wav', 'wma'];
+const pictureFilesExtensions = ['gif', 'png', 'jpg', 'jpeg', 'bmp', 'svg'];
+const videoFilesExtensions = ['avi', 'flv', 'wmv', 'mov', 'mp4'];
+const archiveFilesExtensions = ['tar', 'zip', 'gz', 'bz2', 'rar'];
+const booksFilesExtensions = ['pdf', 'epub', 'fb2'];
+
+function matchFileExtensions(filename, extensions) {
+  const extensionsRegExp = `(${extensions.join('|')})`;
+  return extensions.some((o) => new RegExp(`^.*\.${extensionsRegExp}$`).test(filename.toLowerCase()));
+}
+
+export function getIcon(resource) {
+  if (resource.type === 'dir') {
+    return { svg: dirIcon, fill: defaultFillColor };
+  } else if (matchFileExtensions(resource.name, soundFilesExtensions)) {
+    return { svg: soundFileIcon, fill: `#e53935` };
+  } else if (matchFileExtensions(resource.name, pictureFilesExtensions)) {
+    return { svg: pictureFileIcon, fill: `#e53935` };
+  } else if (matchFileExtensions(resource.name, videoFilesExtensions)) {
+    return { svg: videoFileIcon, fill: `#e53935` };
+  } else if (matchFileExtensions(resource.name, archiveFilesExtensions)) {
+    return { svg: archiveFileIcon, fill: `#616161` };
+  } else if (matchFileExtensions(resource.name, booksFilesExtensions)) {
+    return { svg: booksFileIcon, fill: `#e53935` };
+  } else {
+    return { svg: unknownFileIcon, fill: `#616161` };
+  }
+}

+ 14 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/index.js

@@ -0,0 +1,14 @@
+import api from './api';
+import apiOptions from './apiOptions';
+import capabilities from './capabilities';
+import listViewLayout, { listViewLayout2 } from './list-view-layout';
+import viewLayoutOptions from './view-layout-options';
+
+export default {
+  api,
+  apiOptions,
+  capabilities,
+  listViewLayout,
+  listViewLayout2,
+  viewLayoutOptions,
+};

+ 189 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/list-view-layout.js

@@ -0,0 +1,189 @@
+import fecha from 'fecha';
+import filesize from 'filesize';
+import getMess from './translations';
+import moment from "moment"
+
+const TABLET_WIDTH = 1024;
+const MOBILE_WIDTH = 640;
+
+function formatSize(
+  viewLayoutOptions,
+  { cellData, columnData, columnIndex, dataKey, isScrolling, rowData, rowIndex }
+) {
+  if (typeof cellData !== 'undefined' && viewLayoutOptions.humanReadableSize) {
+    return filesize(cellData);
+  }
+
+  return cellData || '—';
+}
+
+function formatDate(
+  viewLayoutOptions,
+  { cellData, columnData, columnIndex, dataKey, isScrolling, rowData, rowIndex }
+) {
+  if (cellData) {
+    const { dateTimePattern } = viewLayoutOptions;
+    return fecha.format(new Date().setTime(cellData), dateTimePattern);
+  }
+
+  return '';
+}
+
+function formatDate2(
+  viewLayoutOptions,
+  { cellData, columnData, columnIndex, dataKey, isScrolling, rowData, rowIndex }
+) {
+  if (cellData) {
+    const { dateTimePattern } = viewLayoutOptions;
+    return moment(cellData).format("YYYY/MM/DD")
+  }
+
+  return '';
+}
+
+const listViewLayout = viewLayoutOptions => {
+  const getMessage = getMess.bind(null, viewLayoutOptions.locale);
+  return [
+    {
+      elementType: 'Column',
+      elementProps: {
+        key: 'name',
+        dataKey: 'name',
+        width: 300,
+        label: getMessage('title'),
+        flexGrow: 5,
+        cellRenderer: {
+          elementType: 'NameCell',
+          callArguments: [viewLayoutOptions],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: false,
+      },
+    },
+    {
+      elementType: 'Column',
+      elementProps: {
+        key: 'creator',
+        width: 100,
+        dataKey: 'creator',
+        label: '创建人',
+        flexGrow: 1,
+        cellRenderer: {
+          elementType: 'Cell',
+          callArguments: [viewLayoutOptions],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: true,
+      },
+    },
+    {
+      elementType: 'Column',
+      elementProps: {
+        key: 'size',
+        width: 100,
+        dataKey: 'size',
+        label: getMessage('fileSize'),
+        flexGrow: viewLayoutOptions.width > TABLET_WIDTH ? 1 : 0,
+        cellRenderer: {
+          elementType: 'Cell',
+          callArguments: [{ ...viewLayoutOptions, getData: formatSize }],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: true,
+      },
+    },
+    viewLayoutOptions.width > MOBILE_WIDTH && {
+      elementType: 'Column',
+      elementProps: {
+        key: 'modifiedTime',
+        width: 100,
+        dataKey: 'modifiedTime',
+        label: getMessage('lastModified'),
+        flexGrow: 1,
+        cellRenderer: {
+          elementType: 'Cell',
+          callArguments: [{ ...viewLayoutOptions, getData: formatDate }],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: false,
+      },
+    },
+  ];
+};
+
+export default listViewLayout;
+
+export var listViewLayout2 = viewLayoutOptions => {
+  const getMessage = getMess.bind(null, viewLayoutOptions.locale);
+  return [
+    {
+      elementType: 'Column',
+      elementProps: {
+        key: 'name',
+        dataKey: 'name',
+        width: 300,
+        label: getMessage('title'),
+        flexGrow: 5,
+        cellRenderer: {
+          elementType: 'NameCell',
+          callArguments: [viewLayoutOptions],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: false,
+      },
+    },
+    {
+      elementType: 'Column',
+      elementProps: {
+        key: 'creator',
+        width: 120,
+        dataKey: 'creator',
+        label: '创建人',
+        flexGrow: 1,
+        cellRenderer: {
+          elementType: 'Cell',
+          callArguments: [viewLayoutOptions],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: true,
+      },
+    },
+    {
+      elementType: 'Column',
+      elementProps: {
+        key: 'modifiedTime',
+        width: 150,
+        dataKey: 'modifiedTime',
+        label: getMessage('lastModified'),
+        flexGrow: 1,
+        cellRenderer: {
+          elementType: 'Cell',
+          callArguments: [{ ...viewLayoutOptions, getData: formatDate2 }],
+        },
+        headerRenderer: {
+          elementType: 'HeaderCell',
+          callArguments: [viewLayoutOptions],
+        },
+        disableSort: false,
+      },
+    },
+  ];
+};

+ 280 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/translations.js

@@ -0,0 +1,280 @@
+const translations = {
+  cn: {
+    uploading: '正在上传',
+    uploadingItem: '正在上传一个文件',
+    uploadingItems: '正在上传{quantity}个文件',
+    upload: '上传',
+    remove: '删除',
+    download: '下载',
+    see: '查看',
+    rename: '重命名',
+    creating: '正在创建',
+    creatingName: '正在创建 {name}...',
+    create: '创建',
+    createFolder: '创建文件夹',
+    zipping: '正在解压',
+    zippingItem: '正在解压一个文件',
+    zippingItems: '正在解压{quantity}个文件',
+    items: '文件',
+    item: '文件',
+    cancel: '取消',
+    confirm: '确认',
+    folderName: '文件夹名称',
+    files: '文件',
+    fileExist: '文件或文件夹{name}已存在',
+    newName: '新建文件',
+    emptyName: '文件名不能为空',
+    tooLongFolderName: '文件夹名称不能大于255字符',
+    folderNameNotAllowedCharacters: '文件夹包含非法字符',
+    title: '标题',
+    fileSize: '文件大小',
+    lastModified: '更新时间',
+    reallyRemove: '{files}将会被删除,确定要继续吗?',
+    unableReadDir: '无法读取文件夹'
+  },
+  en: {
+    uploading: 'Uploading',
+    uploadingItem: 'Uploading 1 item',
+    uploadingItems: 'Uploading {quantity} items',
+    upload: 'Upload',
+    remove: 'Remove',
+    download: 'Download',
+    see: 'See',
+    rename: 'Rename',
+    creating: 'Creating',
+    creatingName: 'Creating {name}...',
+    create: 'Create',
+    createFolder: 'Create folder',
+    zipping: 'Zipping',
+    zippingItem: 'Zipping 1 item',
+    zippingItems: 'Zipping {quantity} items',
+    items: 'items',
+    item: 'item',
+    cancel: 'Cancel',
+    confirm: 'Confirm',
+    folderName: 'Folder name',
+    files: 'files',
+    fileExist: 'File or folder with name {name} already exists',
+    newName: 'New name',
+    emptyName: 'Name can\'t be empty',
+    tooLongFolderName: 'Folder name can\'t contain more than 255 characters',
+    folderNameNotAllowedCharacters: 'Folder name contains not allowed characters',
+    title: 'Title',
+    fileSize: 'File size',
+    lastModified: 'Last modified',
+    reallyRemove: '{files} will be deleted. Do you really want to proceed?',
+    unableReadDir: 'Unable to read a directory.'
+  },
+
+  de: {
+    uploading: 'Wird hochgeladen',
+    uploadingItem: '1 Element wird geladen',
+    uploadingItems: '{quantity} Elemente werden geladen',
+    upload: 'Hochladen',
+    remove: 'Löschen',
+    download: 'Herunterladen',
+    rename: 'Umbenennen',
+    creating: 'Wird erstellt',
+    creatingName: '{name} wird angelegt...',
+    create: 'Erstellen',
+    createFolder: 'Verzeichnis erstellen',
+    zipping: 'Zippen',
+    items: 'Elemente',
+    item: 'Element',
+    cancel: 'Stornieren',
+    confirm: 'Bestätigen',
+    folderName: ' Verzeichnisname',
+    files: 'Dateien',
+    fileExist: 'Die Datei oder Verzeichnis mit dem Namen {name} existiert bereits',
+    newName: 'Neuer Name',
+    emptyName: 'Der Name darf nicht leer sein',
+    tooLongFolderName: 'Der Verzeichnisname darf nicht mehr als 255 Symbole enthalten',
+    folderNameNotAllowedCharacters: 'Das Verzeichnisname enthält nicht erlaubte Zeichen',
+    title: 'Titel',
+    fileSize: 'Dateigröße',
+    lastModified: 'Zuletzt geändert',
+    reallyRemove: '{files}  wird/werden gelöscht. Möchten Sie wirklich fortfahren?',
+    unableReadDir: 'Ein Directory kann nicht gelesen werden.'
+  },
+
+  fi: {
+    uploading: 'Siirretään palvelimeen',
+    uploadingItem: 'Ladataan 1 nimike',
+    uploadingItems: 'Ladataan {quantity} nimikettä',
+    upload: 'Siirrä palvelimeen',
+    remove: 'Poista',
+    download: 'Lataa',
+    rename: 'Nimeä uudelleen',
+    creating: 'Luodaan',
+    creatingName: 'Luodaan {name}...',
+    create: 'Luo',
+    createFolder: 'Luo kansio',
+    zipping: 'Pakataan',
+    zippingItem: 'Pakataan 1 nimike',
+    zippingItems: 'Pakataan {quantity} nimikettä',
+    items: 'nimikettä',
+    item: 'nimike',
+    cancel: 'Peruuta',
+    confirm: 'Vahvista',
+    folderName: 'Kansion nimi',
+    files: 'tiedostot',
+    fileExist: 'Tiedosto tai kansio, jonka nimi on {name} on jo luotu',
+    newName: 'Uusi nimi',
+    emptyName: 'Nimi ei voi olla tyhjä',
+    tooLongFolderName: 'Kansion nimi saa sisältää enintään 255 merkkiä',
+    folderNameNotAllowedCharacters: 'Kansion nimessä on kiellettyjä merkkejä',
+    title: 'Otsikko',
+    fileSize: 'Tiedostokoko',
+    lastModified: 'Muokattu viimeksi',
+    reallyRemove: '{files} poistetaan. Haluatko varmasti jatkaa?',
+    unableReadDir: 'Hakemistoa ei voi lukea.'
+  },
+
+  hu: {
+    uploading: 'Feltöltés',
+    uploadingItem: '1 elem feltöltése',
+    uploadingItems: '{quantity} elem feltöltése',
+    upload: 'Feltöltés',
+    remove: 'Törlés',
+    download: 'Letöltés',
+    rename: 'Átnevezés',
+    creating: 'Létrehozás',
+    creatingName: '{name} létrehozása...',
+    create: 'Létrehoz',
+    createFolder: 'Mappa létrehozása',
+    zipping: 'Tömörítés',
+    zippingItem: '1 elem tömörítése',
+    zippingItems: '{quantity} elem tömörítése',
+    items: 'elemek',
+    item: 'elem',
+    cancel: 'Mégse',
+    confirm: 'Megerősít',
+    folderName: 'Mappa neve',
+    files: 'Fájlok',
+    fileExist: 'Fájl vagy mappa {name} névvel nem létezik',
+    newName: 'Új név',
+    emptyName: 'Név nem lehet üres',
+    tooLongFolderName: 'Mappa neve nem lehet 255 karakternél hosszabb',
+    folderNameNotAllowedCharacters: 'Mappa neve tiltott karaktereket tartalmaz',
+    title: 'Cím',
+    fileSize: 'Fájl mérete',
+    lastModified: 'Utoljára módosítva',
+    reallyRemove: '{files} fájl törölve lesz. Tényleg folytatni akarja?',
+    unableReadDir: 'Nem lehet olvasni a könyvtárat.'
+  },
+
+  no: {
+    uploading: 'Opplasting',
+    uploadingItem: 'Laster opp 1 element',
+    uploadingItems: 'Laster opp {quantity} elementer',
+    upload: 'Last opp',
+    remove: 'Fjern',
+    download: 'Last ned',
+    rename: 'Gi nytt navn',
+    creating: 'Opprette',
+    creatingName: 'Opprette {name}...',
+    create: 'Opprett',
+    createFolder: 'Opprett mappe',
+    zipping: 'Zipper',
+    zippingItem: 'Zipper 1 element',
+    zippingItems: 'Zipper {quantity} elementer',
+    items: 'elementer',
+    item: 'element',
+    cancel: 'Avbryt',
+    confirm: 'Bekreft',
+    folderName: 'Mappenavn',
+    files: 'filer',
+    fileExist: 'Fil eller mappe med navn {name} eksisterer allerede',
+    newName: 'Nytt navn',
+    emptyName: 'Navnet kan ikke være tomt',
+    tooLongFolderName: 'Mappenavn kan ikke inneholde mer enn 255 tegn',
+    folderNameNotAllowedCharacters: 'Mappenavn inneholder tegn som ikke er tillatt',
+    title: 'Tittel',
+    fileSize: 'Filstørrelse',
+    lastModified: 'Sist endret',
+    reallyRemove: '{files} vil bli slettet. Vil du fortsette?',
+    unableReadDir: 'Kan ikke lese en katalog.'
+  },
+
+  ru: {
+    uploading: 'Загрузка',
+    uploadingItem: 'Загрузка 1 позиции',
+    uploadingItems: 'Загрузка {quantity} позиций',
+    upload: 'Загрузка',
+    remove: 'Remove',
+    download: 'Загрузить',
+    rename: 'Переименовать',
+    creating: 'Создание',
+    creatingName: 'Создание {name}...',
+    create: 'Создать',
+    createFolder: 'Создать папку',
+    zipping: 'Архивирование',
+    zippingItem: 'Архивирование 1 позиции',
+    zippingItems: 'Архивирование {quantity} позиций',
+    items: 'позиции',
+    item: 'позиция',
+    cancel: 'Отмена',
+    confirm: 'Подтвердить',
+    folderName: 'Имя папки',
+    files: 'файлы',
+    fileExist: 'Файл или папка с именем {name} уже существует',
+    newName: 'Новое имя',
+    emptyName: 'Имя не может быть пустым',
+    tooLongFolderName: 'Имя папки не может содержать более 255 символов',
+    folderNameNotAllowedCharacters: 'Имя папки содержит недопустимые символы',
+    title: 'Название',
+    fileSize: 'Размер папки',
+    lastModified: 'Последнее изменение',
+    reallyRemove: '{files} будут удалены. Продолжить?',
+    unableReadDir: 'Невозможно прочитать каталог.'
+  },
+
+  sv: {
+    uploading: 'Överför',
+    uploadingItem: 'Överför 1 post',
+    uploadingItems: 'Överför {quantity} poster',
+    upload: 'Överför',
+    remove: 'Ta bort',
+    download: 'Hämta',
+    rename: 'Ändra namn',
+    creating: 'Skapar',
+    creatingName: 'Skapar {name}...',
+    create: 'Skapa',
+    createFolder: 'Skapa mapp',
+    zipping: 'Komprimerar',
+    zippingItem: 'Komprimerar 1 post',
+    zippingItems: 'Komprimerar {quantity} poster',
+    items: 'poster',
+    item: 'post',
+    cancel: 'Avbryt',
+    confirm: 'Bekräfta',
+    folderName: 'Mappnamn',
+    files: 'filer',
+    fileExist: 'Fil eller mapp med namnet {name} existerar redan',
+    newName: 'Nytt namn',
+    emptyName: 'Namnet får inte vara tomt',
+    tooLongFolderName: 'Mappnamnet kan inte innehålla mer än 255 tecken',
+    folderNameNotAllowedCharacters: 'Mappnamnet innehåller otillåtna tecken',
+    title: 'Rubrik',
+    fileSize: 'Filstorlek',
+    lastModified: 'Senast ändrad',
+    reallyRemove: '{files} kommer att tas bort. Vill du verkligen fortsätta?',
+    unableReadDir: 'Det gick inte att läsa en katalog.'
+  }
+};
+
+export default function getMessage(locale, key, params) {
+  const translationExists = (translations[locale] && translations[locale][key]);
+  const translation = translationExists ? translations[locale][key] : translations['en'][key];
+  if (!params) {
+    return translation;
+  }
+
+  const re = /{\w+}/g;
+  function replace(match) {
+    const replacement = match.slice(1, -1);
+    return params[replacement] ? params[replacement] : '';
+  }
+
+  return translation.replace(re, replace);
+}

+ 18 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/utils/common.js

@@ -0,0 +1,18 @@
+export function normalizeResource(resource) {
+  if (resource) {
+    return {
+      capabilities: resource.capabilities,
+      createdTime: Date.parse(resource.createdTime),
+      id: resource.id,
+      modifiedTime: Date.parse(resource.modifiedTime),
+      name: resource.name,
+      type: resource.type,
+      size: resource.size,
+      parentId: resource.parentId ? resource.parentId : null,
+      ancestors: resource.ancestors ? resource.ancestors : null,
+      creator:resource.creator
+    };
+  } else {
+    return {};
+  }
+}

+ 49 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/utils/download.js

@@ -0,0 +1,49 @@
+import FileSaver from 'file-saver';
+import {GetTokenFromUrl} from "@/utils/utils";
+import {GetFileDownloadUrl} from '@/services/api'
+import {downloadFile} from '../../utils';
+
+// a case when we need to silently download a file using Javascript, and prompt to save it afterwards
+function promptToSaveBlob({ content, name, downloadUrl }) {
+  const DownloadUrl = `${downloadUrl}&JWT-TOKEN=${GetTokenFromUrl()}`;
+  if (downloadUrl) {
+    // const iframeId = 'oc-fm--filemanager-download-iframe';
+    // let iframeDOMNode = document.getElementById(iframeId);
+    //
+    // if (!iframeDOMNode) {
+    //   iframeDOMNode = document.createElement('iframe');
+    //   iframeDOMNode.style.display = 'none';
+    //   iframeDOMNode.id = iframeId;
+    //   document.body.appendChild(iframeDOMNode);
+    // }
+    //
+    // iframeDOMNode.src = downloadUrl;
+    GetFileDownloadUrl(DownloadUrl).then(ret => {
+      if (ret !== false && ret.status !== 600) {
+        downloadFile(ret, name)
+      }
+    });
+  } else {
+    const blob = new Blob([content], { type: 'octet/stream' });
+    FileSaver.saveAs(blob, name);
+  }
+}
+
+// a case when we trigger a direct download in browser
+// used in google drive' connector
+function triggerHiddenForm({ downloadUrl, target = '_self' }) {
+  const form = document.createElement("form");
+  form.action = downloadUrl;
+  form.target = target;
+  form.method = 'GET';
+
+  document.body.appendChild(form);
+  form.submit();
+  document.body.removeChild(form);
+}
+
+export {
+  promptToSaveBlob,
+  triggerHiddenForm
+};
+

+ 61 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/utils/notifications.js

@@ -0,0 +1,61 @@
+import { find, findIndex, extend } from 'lodash';
+
+function addNotification(notifications, id, props) {
+  const index = findIndex(notifications, (o) => o.id === id);
+  if (index !== -1) {
+    console.error(`Can't add notification: ${id} already exists`);
+    return notifications;
+  }
+  return notifications.concat([{ id, children: (props.children || []), ...props }]);
+}
+
+function updateNotification(notifications, id, props) {
+  return notifications.map(o => {
+    if (o.id !== id) {
+      return o;
+    }
+
+    return extend({}, o, props);
+  });
+}
+
+function getNotification(notifications, id) {
+  return find(notifications, (o) => o.id === id);
+}
+
+function removeNotification(notifications, id) {
+  return notifications.filter(o => o.id !== id);
+}
+
+function addChild(notificationChildren, id, element) {
+  return notificationChildren.concat([{ id, element }]);
+}
+
+function removeChild(notificationChildren, id) {
+  return notificationChildren.filter((o) => o.id !== id);
+}
+
+function updateChild(notificationChildren, id, element) {
+  return notificationChildren.map(o => {
+    if (o.id !== id) {
+      return o;
+    }
+
+    return extend({}, o, { id, ...element });
+  });
+}
+
+function getChild(notificationChildren, id) {
+  return find(notificationChildren, (o) => o.id === id);
+}
+
+export default {
+  addNotification,
+  updateNotification,
+  removeNotification,
+  getNotification,
+  addChild,
+  removeChild,
+  updateChild,
+  getChild
+};

+ 28 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/utils/onFailError.js

@@ -0,0 +1,28 @@
+import notifUtils from './notifications';
+
+export default function onFailErrors({
+  getNotifications,
+  label,
+  notificationId,
+  updateNotifications,
+  message
+}) {
+  const notifications = getNotifications();
+  let newNotifications = notifUtils.removeNotification(notifications, notificationId);
+
+  const newNotification = {
+    title: message || `${label} error`,
+    minimizable: false,
+    closable: true,
+    children: [],
+    onHide: _ => updateNotifications(notifUtils.removeNotification(notifications, notificationId))
+  };
+
+  const notification = notifUtils.getNotification(notifications, notificationId);
+
+  newNotifications = notification ?
+    notifUtils.updateNotification(notifications, notificationId, newNotification) :
+    notifUtils.addNotification(notifications, notificationId, newNotification);
+
+  updateNotifications(newNotifications);
+}

+ 32 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/utils/see.js

@@ -0,0 +1,32 @@
+import FileSaver from 'file-saver';
+import { GetTokenFromUrl } from '@/utils/utils';
+import { GetFileDownloadUrl } from '@/services/api';
+// a case when we need to silently download a file using Javascript, and prompt to save it afterwards
+function promptToSaveBlob({ content, name, downloadUrl }) {
+  downloadUrl = `${downloadUrl}&JWT-TOKEN=${GetTokenFromUrl()}`;
+  GetFileDownloadUrl(downloadUrl).then(ret => {
+    console.log(ret);
+    if (window.InvokeUnityFileOpener) {
+      window.InvokeUnityFileOpener(ret);
+    } else {
+      // window.location.href = `${ret}`;
+      window.open(ret);
+    }
+  });
+}
+
+// a case when we trigger a direct download in browser
+// used in google drive' connector
+function triggerHiddenForm({ downloadUrl, target = '_self' }) {
+  debugger;
+  const form = document.createElement('form');
+  form.action = downloadUrl;
+  form.target = target;
+  form.method = 'GET';
+
+  document.body.appendChild(form);
+  form.submit();
+  document.body.removeChild(form);
+}
+
+export { promptToSaveBlob, triggerHiddenForm };

+ 27 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/utils/upload.js

@@ -0,0 +1,27 @@
+async function readLocalFile() {
+  return new Promise((resolve, reject) => {
+    const uploadInput = document.createElement("input");
+
+    uploadInput.addEventListener('change', _ => {
+      const file = uploadInput.files[0];
+      resolve({
+        type: file.type,
+        name: file.name,
+        file
+      });
+    });
+
+    // This input element in IE11 becomes visible after it is added on the page
+    // Hide an input element
+    uploadInput.style.visibility = 'hidden';
+
+    uploadInput.type = "file";
+    document.body.appendChild(uploadInput);
+    uploadInput.click();
+    document.body.removeChild(uploadInput);
+  });
+}
+
+export {
+  readLocalFile
+}

+ 10 - 0
src/Project/pages/DataMeter/Model/filemanager-connector/view-layout-options.js

@@ -0,0 +1,10 @@
+import { getIcon } from './icons';
+
+export default {
+  locale: 'cn',
+  initialSortBy: 'name',
+  initialSortDirection: 'ASC',
+  dateTimePattern: 'YYYY-MM-DD HH:mm:ss',
+  humanReadableSize: true,
+  getIcon
+};

+ 18 - 72
src/Project/pages/DataMeter/Model/index.tsx

@@ -6,9 +6,13 @@ import Monitor from './Monitor';
 import style from '../index.less';
 import AlarmCenter from './AlarmCenter';
 import MessageCenter from './MessageCenter';
+import DataCenter from './DataCenter';
+import FileManagement from './FileManagement';
+import Other from './Other';
+import PlanManagement from './PlanManagement';
 
 function Model(props: DataMeter.IModelsProps) {
-  const { layout, edit, removeModel } = props;
+  const { layout, projectId } = props;
   let content: React.ReactElement;
   let path: number;
   switch (layout.key) {
@@ -22,80 +26,22 @@ function Model(props: DataMeter.IModelsProps) {
       return <Monitor {...props} />;
     case 'AlarmCenter': //报警中心
       return <AlarmCenter {...props} />;
-    // case 'FileManagement':
-    //   content = (
-    //     <FileManagement
-    //       key={layout.i}
-    //       projectId={projectId}
-    //       {...props}
-    //       child={layout.child}
-    //     />
-    //   );
-    //   break;
-    // case 'DataCenter':
-    //   let originList = [];
-    //   let configList = (chartConfigList || []).map((config) => {
-    //     var data = layout.child.find((child) => child.key === config.id);
-    //     return {
-    //       ...config,
-    //       title: config.name,
-    //       key: config.id,
-    //       type: 'chartConfig',
-    //       show: data ? data.show : true,
-    //     };
-    //   });
-    //   layout.child.forEach((layout) => {
-    //     if (!layout.children && layout.type != 'chartConfig') {
-    //       originList.push(layout);
-    //     } else {
-    //       // 有子级则只将子级加入tabs
-    //       originList = originList.concat(layout.children);
-    //     }
-    //   });
-
-    //   originList = originList.concat(configList);
-    //   content = (
-    //     <DataCenter
-    //       projectId={projectId}
-    //       key={layout.i + projectId}
-    //       {...props}
-    //       child={
-    //         subModule == 0 ? originList : chartConfigList ? originList : []
-    //       }
-    //       layout={layout}
-    //       setActive={setActive}
-    //     />
-    //   );
-    //   break;
+    case 'FileManagement':
+      content = <FileManagement {...props} />;
+      break;
+    case 'DataCenter':
+      content = <DataCenter {...props} key={layout.i + projectId} />;
+      break;
     case 'MessageCenter': //新闻动态
       return <MessageCenter {...props} />;
-    // case 'Other':
-    //   content = (
-    //     <Other
-    //       key={layout.i}
-    //       {...props}
-    //       projectId={projectId}
-    //       child={layout.child}
-    //       layout={layout}
-    //       setActive={setActive}
-    //       subModule={subModule}
-    //     />
-    //   );
-    //   break;
-    // case 'PlanManagement':
-    //   content = (
-    //     <PlanManagement
-    //       key={layout.i}
-    //       projectId={projectId}
-    //       {...props}
-    //       child={layout.child}
-    //       layout={layout}
-    //       setActive={setActive}
-    //     />
-    //   );
-    //   break;
+    case 'Other':
+      content = <Other {...props} />;
+      break;
+    case 'PlanManagement':
+      content = <PlanManagement {...props} />;
+      break;
     default:
-      content = <div>loading...</div>;
+      content = <div>unknown model [{layout.key}]</div>;
       break;
   }
 

+ 89 - 0
src/Project/pages/DataMeter/MouldDrawerLeft.tsx

@@ -0,0 +1,89 @@
+import React, { useRef } from 'react';
+import { Button, Empty } from 'antd';
+import styles from './index.less';
+import ReactZmage from 'react-zmage';
+import { useRequest } from '@umijs/max';
+import { getMouldList } from '@/Project/services/DataMeter';
+
+function MouldDrawerLeft(props: any) {
+  const { onClose, visible, mouldSave, projectId, subModule } = props;
+  const mouldListRequest = useRequest(getMouldList, {
+    defaultParams: [
+      {
+        project_id: projectId,
+        module: 1,
+        sub_module: subModule,
+      },
+    ],
+  });
+  const mouldList = mouldListRequest.data || [];
+  if (!visible) return null;
+  return (
+    <div className={styles.mouldList} style={{ top: 10, right: 15 }}>
+      <div className={styles.header}>
+        <div className={styles.title}>模板列表</div>
+        <div className={styles.closeIcon} onClick={onClose} />
+        {/* <Icon type="close" onClick={onClose} /> */}
+      </div>
+      <div className={styles.content}>
+        {mouldList.length == 0 && <Empty style={{ marginTop: 200 }} />}
+        {mouldList.map((item: any) => {
+          return (
+            <MouldItem
+              key={item.id}
+              item={item}
+              mouldSave={mouldSave}
+            ></MouldItem>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+function MouldItem(props: any) {
+  const { item, mouldSave } = props;
+  const zImageRef = useRef<any>();
+  const zImageClick = () => {
+    zImageRef?.current?.coverRef?.current?.click();
+  };
+  return (
+    <div className={styles.item}>
+      <img
+        className={styles.img}
+        src={item.cover}
+        onClick={() => {
+          zImageClick();
+        }}
+      ></img>
+
+      <div className={styles.btnGroup}>
+        <div className={styles.name}>{item.name}</div>
+        <Button
+          type="primary"
+          onClick={() => {
+            mouldSave?.(item);
+          }}
+        >
+          应用
+        </Button>
+      </div>
+      <ReactZmage
+        ref={zImageRef}
+        controller={{
+          close: true,
+          rotate: true,
+          zoom: false,
+          download: false,
+          flip: false,
+          pagination: false,
+        }}
+        backdrop="rgba(255,255,255,0.5)"
+        // style={{ display: 'none' }}
+        src={item.cover}
+        onBrowsing={(state) => {}}
+      />
+    </div>
+  );
+}
+
+export default MouldDrawerLeft;

+ 1 - 0
src/Project/pages/DataMeter/index.less

@@ -38,6 +38,7 @@
   // background: url('@/Project/assets/dataMeter/conLeft.png') no-repeat 100% 100%;
   background-size: 100% 100%;
   position: relative;
+  height: 100%;
 
   .controlBox {
     position: absolute;

+ 115 - 37
src/Project/pages/DataMeter/index.tsx

@@ -11,13 +11,17 @@ import {
   U3D_PATH_STATE,
 } from './config';
 import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { Modal } from 'antd';
+import { message, Modal } from 'antd';
 import Model from './Model';
 import { useModel, useRequest } from '@umijs/max';
 import {
   getLayoutOptions,
+  queryChartChild,
   saveLayoutOptions,
 } from '@/Project/services/DataMeter';
+import CreateMould from './CreateMould';
+import DrawerLeft from './DrawerLeft';
+import MouldDrawerLeft from './MouldDrawerLeft';
 
 const gridWidth = document.documentElement.clientWidth;
 
@@ -56,11 +60,11 @@ function DataMeter(props: DataMeter.IProps) {
   //创建模板弹窗状态
   const [createMouldVisible, setCreateMouldVisible] = useState(false);
 
-    const layoutRequest = useRequest(getLayoutOptions, {
-      defaultParams: [
-        {
-          projectId: projectId || 0,
-          module: 1,
+  const layoutRequest = useRequest(getLayoutOptions, {
+    defaultParams: [
+      {
+        projectId: projectId || 0,
+        module: 1,
         sub_module: subModule,
         is_default: 0,
       },
@@ -69,6 +73,28 @@ function DataMeter(props: DataMeter.IProps) {
       setLayout(data.config_json);
     },
   });
+
+  const chartChildRequest = useRequest(queryChartChild, {
+    defaultParams: [projectId],
+    cacheKey: 'queryChartChild',
+    staleTime: -1,
+  });
+  const createMouldHandleOk = (value: any) => {
+    let params = {
+      module: 1,
+      sub_module: subModule,
+      project_id: projectId,
+      is_template: 1,
+      config_json: JSON.stringify(layout),
+      name: value.name,
+    };
+    setCreateMouldVisible(false);
+    // TODO: 通知unity创建模板
+    // setTimeout(() => {
+    //   UnityAction.sendMsg('mouldConfig', JSON.stringify(params));
+    // }, 500);
+  };
+
   // 布局发生改变回调函数
   const onLayoutChange = (newLayout: DataMeter.ILayout[]) => {
     // console.log(newLayout);
@@ -119,6 +145,41 @@ function DataMeter(props: DataMeter.IProps) {
     });
     setModelCheck(checked);
   };
+  // 添加子模块
+  const addModel = (modelKey: string) => {
+    // setDragEnter(false);
+    console.log('=====modelkey=====', modelKey);
+    if (!modelKey) return;
+    if (
+      modelKey == 'FileManagement' &&
+      layout.find((item) => item.key == 'FileManagement')
+    ) {
+      message.error('只能有一个文件管理组件!');
+      return;
+    }
+    var [mainModel, subModel, subModel2] = modelKey.split(';;');
+    let currentIndex = Number(layout[0].i) + 1;
+    // 默认全选子模块
+    let child = CHILD_MAP[mainModel]
+      ? JSON.parse(JSON.stringify(CHILD_MAP[mainModel]))
+      : undefined;
+    let childKey = child && child[0] && child[0].key;
+    let newData = {
+      i: currentIndex + '',
+      key: mainModel,
+      active: subModel2 || subModel || childKey,
+      w: 14,
+      h: 7,
+      x: Infinity,
+      y: Infinity,
+      child,
+      moved: true,
+      static: true,
+    };
+    layout.unshift(newData);
+    console.log('add model=========', layout);
+    setLayout(JSON.parse(JSON.stringify(layout)));
+  };
   // 保存配置
   const saveLayout = async (newLayout: DataMeter.ILayout[], isDefault = 0) => {
     let id,
@@ -149,6 +210,18 @@ function DataMeter(props: DataMeter.IProps) {
       });
     }
   };
+  const saveMould = (mouldItem: any) => {
+    Modal.confirm({
+      title: '提示',
+      content: `是否应用-${mouldItem.name}-为个人驾驶舱模板?`,
+      okText: '确定',
+      cancelText: '取消',
+      onOk: () => {
+        setLayout(JSON.parse(mouldItem.config_json));
+      },
+      onCancel() {},
+    });
+  };
   const models = useMemo(() => {
     return layout.map((item) => {
       const setActive = (key: number | string) => {
@@ -156,12 +229,16 @@ function DataMeter(props: DataMeter.IProps) {
         // saveLayout(layout);
         setLayout([...layout]);
       };
+      let child = item.child || [];
+      if (item.key == 'DataCenter') {
+        child = chartChildRequest.data || [];
+      }
       return (
         <div key={item.i}>
           <Model
             key={item.i}
             hasModel={hasModel}
-            child={item.child || []}
+            child={child}
             projectId={projectId}
             removeModel={removeModel}
             edit={edit}
@@ -174,7 +251,7 @@ function DataMeter(props: DataMeter.IProps) {
         </div>
       );
     });
-  }, [layout]);
+  }, [layout, chartChildRequest.data]);
 
   return (
     <div
@@ -182,33 +259,34 @@ function DataMeter(props: DataMeter.IProps) {
       // onDragEnd={onDragEnd}
       style={{ minHeight: '100vh' }}
     >
-      {/* <CreateMould
-          visible={createMouldVisible}
-          handleCancel={() => setCreateMouldVisible(false)}
-          handleOk={createMouldHandleOk}
-        ></CreateMould>
-        <DrawerLeft
-          onClose={() => {
-            setVisible(false);
-          }}
-          layout={layout}
-          dragActive={dragActive}
-          addModel={addModel}
-          removeModel={removeModel}
-          // onDrag={onHandleDrag}
-          visible={visible}
-          subModule={subModule}
-          dataCenterChild={dataCenterChild}
-        />
-        {mouldVisible && <div className={style.mask}></div>}
-        <MouldDrawerLeft
-          mouldList={mouldList}
-          onClose={() => {
-            setMouldVisible(false);
-          }}
-          visible={mouldVisible}
-          mouldSave={saveMould}
-        ></MouldDrawerLeft> */}
+      <CreateMould
+        visible={createMouldVisible}
+        handleCancel={() => setCreateMouldVisible(false)}
+        handleOk={createMouldHandleOk}
+      ></CreateMould>
+      <DrawerLeft
+        onClose={() => {
+          setVisible(false);
+        }}
+        layout={layout}
+        addModel={addModel}
+        removeModel={removeModel}
+        visible={visible}
+        subModule={subModule}
+        dataCenterChild={dataCenterChild}
+      />
+      {mouldVisible && <div className={style.mask}></div>}
+      <MouldDrawerLeft
+        projectId={projectId}
+        subModule={subModule}
+        onClose={() => {
+          setMouldVisible(false);
+        }}
+        visible={mouldVisible}
+        mouldSave={saveMould}
+      ></MouldDrawerLeft>
+      {/*
+       */}
       {/*         
         <EditMould
           title="编辑子模块"
@@ -225,8 +303,8 @@ function DataMeter(props: DataMeter.IProps) {
       <div className={style.gridBox}>
         <GridLayout
           className="layout"
-          // isDraggable={edit}
-          // isResizable={edit}
+          isDraggable={edit}
+          isResizable={edit}
           layout={layout}
           onLayoutChange={onLayoutChange}
           cols={COL_COLS}

+ 8 - 3
src/Project/pages/DataMeter/typings.d.ts

@@ -48,8 +48,13 @@ declare namespace DataMeter {
     width?: number;
   }
   interface IProjectListProps {
-    edit:boolean,
-    subModule: number,
-    layout: ILayout,
+    edit: boolean;
+    subModule: number;
+    layout: ILayout;
+  }
+  interface ICreateMouldProps {
+    visible: boolean;
+    handleCancel: () => void;
+    handleOk: (values: any) => void;
   }
 }

+ 86 - 3
src/Project/services/DataMeter.ts

@@ -281,6 +281,59 @@ export async function getProjectList(params: any) {
   return response;
 }
 
+const PLAN_TYPE = {
+  15: 1,
+  3: 3,
+  12: 4,
+};
+
+// planType 1-到货 3-安装 4-调试
+export async function getProjectRealProgress(params: any) {
+  const calcProgress = (data: any) => {
+    let count = 0,
+      total = 0;
+    data.forEach((item: any) => {
+      total += item.TotalCount;
+      count += item.RealCount;
+    });
+    return ((count / total) * 100).toFixed(2);
+  };
+  var keys = Object.keys(PLAN_TYPE);
+  let realProgress: any = {};
+  for (let i = 0; i < keys.length; i++) {
+    let k = Number(keys[i]);
+    var response = await request(`/api/v1/bim/plan/info/${params.projectId}`, {
+      params: { ...params, type: PLAN_TYPE[k as keyof typeof PLAN_TYPE] },
+    });
+
+    if (response) {
+      let tempRealProgress = response.data.Task.map((item: any) => ({
+        ...item,
+        RealCount: item.FinishList.length || 0,
+        TotalCount: item.DeviceCodes ? item.DeviceCodes.length : 0,
+        children: null,
+      }));
+      realProgress[k] = calcProgress(tempRealProgress);
+    }
+  }
+  return { data: realProgress };
+}
+
+export async function getProjectPlanProgress(projectId: string | number) {
+  const response = await request(`/project-plan-progress/${projectId}`);
+  var progress = response.data;
+  Object.keys(progress).forEach((k) => {
+    if (
+      progress[k] == '0001-01-01T00:00:00Z' ||
+      progress[k] == '0001-01-01 00:00:00' ||
+      progress[k] == '0001-01-01 00:00:00 +0000 UTC'
+    ) {
+      progress[k] = '';
+    }
+  });
+  return response;
+}
+
 //获取巡检结果
 export async function getAutoPatrol(params: any) {
   return request(`/api/v1/patrol/auto/data/${params.projectId}`);
@@ -321,11 +374,41 @@ export async function getAutoPatrolByRouteId(params: any) {
 }
 
 export async function queryConfigList(params: any) {
-  return request(`/chart/sync/info/${params.projectId}`, {
+  return request(`/api/v1/chart/sync/info/${params.projectId}`, {
     params: params,
   });
 }
 
 export async function queryTemplateList() {
-  return request(`/chart/list?pageSize=999`);
-}
+  return request(`/api/v1/chart/list?pageSize=9999`);
+}
+
+export async function queryChartChild(projectId: string | number) {
+  const {
+    data: { list: tplList },
+  } = await queryTemplateList();
+  const {
+    data: { list: configList1 },
+  } = await queryConfigList({
+    projectId,
+    isNew: 1,
+    pageSize: 9999,
+  });
+  const {
+    data: { list: configList2 },
+  } = await queryConfigList({ projectId, pageSize: 9999 });
+
+  let list: any = [];
+  configList2.map((item: any) => {
+    item.options = JSON.parse(item.options);
+    list.push(item);
+  });
+  configList1.map((item: any) => {
+    let template = tplList.find((tpl: any) => tpl.ID == item.type);
+    item.options = JSON.parse(item.options);
+    item.template = template;
+    list.push(item);
+  });
+
+  return { data: list };
+}

+ 10 - 0
src/Project/services/FileAdmin.ts

@@ -0,0 +1,10 @@
+import { request } from '@umijs/max';
+
+export async function queryProjectFileList(params: any) {
+  return request(
+    `/project-file/${params.projectId}/${params.fileType}/${params.deviceCode}`,
+    {
+      params,
+    },
+  );
+}

+ 79 - 2
src/Project/services/project.ts

@@ -1,8 +1,85 @@
+import moment from 'moment';
 import { request } from 'umi';
 
 //获取项目列表
-export async function getProjectList(params: any): Promise<Api.IResponseStructure> {
-  return request(`/api/v2/project`, {
+export async function getProjectList(
+  params: any,
+): Promise<Api.IResponseStructure> {
+  const response = await request(`/api/v2/project`, {
+    params: params,
+  });
+  let nowDate = new Date();
+  (response.data.list || []).map((item: Api.IProject) => {
+    let type;
+    if (!item.EndDate) {
+      type = 2;
+    } else {
+      type = nowDate >= new Date(item.EndDate) ? 2 : 1;
+    }
+    item.type = type;
+  });
+  return response;
+}
+
+export async function getDeviceRealData(params: any) {
+  return request(`/api/v1/jinke-cloud/device/current-data`, {
+    method: 'POST',
+    body: params,
+  });
+}
+
+export async function getDeviceRealDataByTime(params: any) {
+  return request(`/api/v1/jinke-cloud/db/device/history-data`, {
+    method: 'GET',
     params,
   });
 }
+export async function queryFormCell(
+  projectId: string | number,
+  formName: string,
+) {
+  const res = await request(
+    `/api/v1/api/v1/runtime_form/cell/to/chart/${projectId}/${formName}`,
+  );
+  return res;
+}
+
+const CACHE: any = {};
+// 查询图表对应的表单数据-最新数据
+export async function queryFormCurrentData(params: any) {
+  const { projectId, formName, titles } = params;
+  if (!CACHE[formName]) {
+    const resCell = await queryFormCell(projectId, formName);
+    CACHE[formName] = resCell.data || [];
+  }
+  let cells = titles.map((t: string) => {
+    let cell = CACHE[formName].find((item: any) => item.title == t);
+    return cell;
+  });
+  const { data } = await request(
+    `/api/v1/runtime_form/chart/current/${projectId}/${formName}`,
+  );
+
+  return cells.map((item: any) => ({
+    key: item.cell_key,
+    value: data[item.cell_key],
+    title: item.title,
+  }));
+}
+export async function queryFormHistoryData2(params: any) {
+  const { data } = await request(`/api/v1/jinke-cloud/db/device/form-chart`, {
+    method: 'POST',
+    body: params,
+  });
+  return Object.keys(data).map((key) => {
+    let [tableName, title] = key.split('|');
+    let dataValue = data[key];
+    return {
+      name: title,
+      data: dataValue.map((item: any) => ({
+        htime: moment(item.time).format('YYYY-MM-DD HH:mm:ss'),
+        val: item.key,
+      })),
+    };
+  });
+}

+ 5 - 0
src/Project/utils/index.ts

@@ -0,0 +1,5 @@
+import { LocalService, STORAGE_TYPE } from '@/Frameworks/SysStorage';
+
+export function getToken(): string {
+  return LocalService.getItem(STORAGE_TYPE.token);
+}

+ 17 - 1
src/models/project.ts

@@ -1,11 +1,27 @@
 // 全局共享数据示例
-import { useState } from 'react';
+import { getProjectList } from '@/Project/services/project';
+import { useRequest } from '@umijs/max';
+import { useCallback, useState } from 'react';
 
 const useProject = () => {
+  // 当前选中项目
   const [project, setProject] = useState<Api.IProject>();
+  // 项目列表
+  const projectRequest = useRequest(getProjectList);
+  const projectList: Api.IProject[] = projectRequest.data?.list || [];
+
+  const getProject = useCallback(
+    (id: number | string) => {
+      return projectList.find((project) => project.ID == id) || null;
+    },
+    [projectRequest.data],
+  );
+
   return {
     project,
     setProject,
+    projectList,
+    getProject,
   };
 };
 

+ 372 - 9
yarn.lock

@@ -1303,6 +1303,13 @@
   dependencies:
     regenerator-runtime "^0.13.11"
 
+"@babel/runtime@^7.1.2":
+  version "7.20.13"
+  resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
+  integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
+  dependencies:
+    regenerator-runtime "^0.13.11"
+
 "@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
   version "7.20.7"
   resolved "https://registry.npmmirror.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@@ -1742,6 +1749,53 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
+"@opuscapita/react-filemanager-connector-node-v1@^1.1.0-beta.6":
+  version "1.1.12"
+  resolved "https://registry.npmmirror.com/@opuscapita/react-filemanager-connector-node-v1/-/react-filemanager-connector-node-v1-1.1.12.tgz#e6b353e1b20db644f421b1ea59356358b32ee7da"
+  integrity sha512-MEUYGUkV00uwzQNWec+l5f6SiiOfMR/Xnqk6IioR5ZagZ+f3BbHV0ILuYzNc+P4Rhuh3R9cv3W9AB4MtV8wWVg==
+  dependencies:
+    fecha "2.3.1"
+    file-saver "1.3.3"
+    filesize "3.5.11"
+    nanoid "1.0.0"
+    range-parser "1.2.0"
+    sanitize-filename "1.6.1"
+    superagent "3.8.3"
+
+"@opuscapita/react-filemanager@^1.1.0-beta.6":
+  version "1.1.12"
+  resolved "https://registry.npmmirror.com/@opuscapita/react-filemanager/-/react-filemanager-1.1.12.tgz#27828d86e4f84e118c406c9d3ce347696fbfe6ed"
+  integrity sha512-09OGtwMdaIB1qpBKpRdyLREIPy2K2Cii1OhLSFRihQQXkpURYsB21B2DA9ZTCfb7e7tc9Gwc9ccpY+sygeJSOQ==
+  dependencies:
+    "@opuscapita/react-svg" "2.0.1"
+    "@opuscapita/svg-icons" "1.1.1"
+    core-js "2.5.0"
+    detect-it "3.0.3"
+    lodash "4.17.21"
+    nanoid "1.0.0"
+    prop-types "15.8.1"
+    range-parser "1.2.0"
+    react-click-outside "3.0.0"
+    react-contextmenu "2.8.0"
+    react-dnd "2.5.4"
+    react-dnd-html5-backend "2.5.4"
+    react-dnd-scrollzone "4.0.0"
+    react-dropzone "4.2.1"
+    react-sortable-hoc "0.6.8"
+    react-virtualized "9.12.0"
+    sanitize-filename "1.6.1"
+    superagent "3.8.3"
+
+"@opuscapita/react-svg@2.0.1":
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/@opuscapita/react-svg/-/react-svg-2.0.1.tgz#9ae255c5dcf8142040b70d47426cb2a8ec384010"
+  integrity sha512-NCg9rmnIH+Hmd74TzBomTzVh3tv6RLjIp3nwCT7pCkklpaOOp8RamxelfVm3ThZ/7vInZ4qnXoPGk4staTTFHA==
+
+"@opuscapita/svg-icons@1.1.1":
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/@opuscapita/svg-icons/-/svg-icons-1.1.1.tgz#9e0ec9973e9b164e5db8ce5f07e28f4a818db1b5"
+  integrity sha512-t87XxvoRTSp9GB3MMLhljBXuJ3Yr/hQlEWbCpoTEWn+C1G9Tqt+civLDwHoptnUbe1m12El/EtMfzYaMInDzVg==
+
 "@parcel/css-darwin-arm64@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@parcel/css-darwin-arm64/-/css-darwin-arm64-1.9.0.tgz#5a020c604249180afcf69ce0f6978b807e2011b3"
@@ -2823,6 +2877,11 @@ arrify@^1.0.1:
   resolved "https://registry.npmmirror.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
 
+asap@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+
 asn1.js@^5.2.0:
   version "5.4.1"
   resolved "https://registry.npmmirror.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@@ -2861,6 +2920,13 @@ atomic-sleep@^1.0.0:
   resolved "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
   integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
 
+attr-accept@^1.0.3:
+  version "1.1.3"
+  resolved "https://registry.npmmirror.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
+  integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
+  dependencies:
+    core-js "^2.5.0"
+
 autoprefixer@^10.4.6:
   version "10.4.13"
   resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8"
@@ -3000,6 +3066,14 @@ babel-preset-jest@^28.1.3:
     babel-plugin-jest-hoist "^28.1.3"
     babel-preset-current-node-syntax "^1.0.0"
 
+babel-runtime@^6.11.6, babel-runtime@^6.23.0:
+  version "6.26.0"
+  resolved "https://registry.npmmirror.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+  integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
+  dependencies:
+    core-js "^2.4.0"
+    regenerator-runtime "^0.11.0"
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -3361,7 +3435,7 @@ colorette@^2.0.19:
   resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
   integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
 
-combined-stream@^1.0.8:
+combined-stream@^1.0.6, combined-stream@^1.0.8:
   version "1.0.8"
   resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
   integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -3398,6 +3472,11 @@ common-path-prefix@^3.0.0:
   resolved "https://registry.npmmirror.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0"
   integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==
 
+component-emitter@^1.2.0:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
 compute-scroll-into-view@^1.0.20:
   version "1.0.20"
   resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43"
@@ -3428,6 +3507,11 @@ convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.7.0:
   resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
   integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
 
+cookiejar@^2.1.0:
+  version "2.1.4"
+  resolved "https://registry.npmmirror.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
+  integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
+
 copy-anything@^2.0.1:
   version "2.0.6"
   resolved "https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@@ -3454,11 +3538,21 @@ core-js-pure@^3.8.1:
   resolved "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.27.1.tgz#ede4a6b8440585c7190062757069c01d37a19dca"
   integrity sha512-BS2NHgwwUppfeoqOXqi08mUqS5FiZpuRuJJpKsaME7kJz0xxuk0xkhDdfMIlP/zLa80krBqss1LtD7f889heAw==
 
+core-js@2.5.0:
+  version "2.5.0"
+  resolved "https://registry.npmmirror.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086"
+  integrity sha512-mAPLSnIVZAwVEf8OZtnNcF2BL1d6DHV3EvIWj46UDBYNAqLxx7mLLpQxe8/1vtrkzt1KIyjmOqOG3pa+bZf7Fw==
+
 core-js@3.22.4:
   version "3.22.4"
   resolved "https://registry.npmmirror.com/core-js/-/core-js-3.22.4.tgz#f4b3f108d45736935aa028444a69397e40d8c531"
   integrity sha512-1uLykR+iOfYja+6Jn/57743gc9n73EWiOnSJJ4ba3B4fOEYDBv25MagmEZBxTp5cWq4b/KPx/l77zgsp28ju4w==
 
+core-js@^2.4.0, core-js@^2.5.0:
+  version "2.6.12"
+  resolved "https://registry.npmmirror.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
+
 core-util-is@~1.0.0:
   version "1.0.3"
   resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@@ -3661,7 +3755,7 @@ dayjs@1.x, dayjs@^1.11.1, dayjs@^1.11.2, dayjs@^1.11.4:
   resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
   integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
 
-debug@^3.2.6:
+debug@^3.1.0, debug@^3.2.6:
   version "3.2.7"
   resolved "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
   integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -3729,11 +3823,26 @@ des.js@^1.0.0:
     inherits "^2.0.1"
     minimalistic-assert "^1.0.0"
 
+detect-hover@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/detect-hover/-/detect-hover-1.0.3.tgz#7392507cbced1bedc9f129a56b197c1eef9076c3"
+  integrity sha512-HtLoY+tClgYucJNiovNICGWFp9nOGVmHY44s7L62iPqORXM9vujeWFaVcqtA7XRvp/2Y+4RBUfHbDKFGN+xxZQ==
+
 detect-indent@^6.0.0:
   version "6.1.0"
   resolved "https://registry.npmmirror.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
   integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
 
+detect-it@3.0.3:
+  version "3.0.3"
+  resolved "https://registry.npmmirror.com/detect-it/-/detect-it-3.0.3.tgz#8e13daa0b62126150cbf76d083a1d34d1b07d071"
+  integrity sha512-oTvnQQlTS6bmyy6xPlUGOYZSLOZWu4XgCjxJVX8LsXHUtpVsXd+RM2Mj6g0qTDK/N/QzaT1uhuL+cOZ/RpXftA==
+  dependencies:
+    detect-hover "^1.0.2"
+    detect-passive-events "^1.0.4"
+    detect-pointer "^1.0.2"
+    detect-touch-events "^2.0.1"
+
 detect-libc@^1.0.3:
   version "1.0.3"
   resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
@@ -3749,6 +3858,21 @@ detect-node@^2.0.4:
   resolved "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
   integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
 
+detect-passive-events@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.npmmirror.com/detect-passive-events/-/detect-passive-events-1.0.5.tgz#ce324db665123bef9e368b8059ff95d95217cc05"
+  integrity sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA==
+
+detect-pointer@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/detect-pointer/-/detect-pointer-1.0.3.tgz#27d86d0837951e6c5a0785c8a7fddd9cf5754e08"
+  integrity sha512-d0o/Puo3fiGSCXy6H039h9Kwz+mmYCGKZ/qtPFnpN3WfsumjC1C9b5KKvRu+aYnfdI8peqN/iAe7dPd85qIt2g==
+
+detect-touch-events@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/detect-touch-events/-/detect-touch-events-2.0.2.tgz#541cc49b05ca544726a3bf2802c5b23e53aeeebb"
+  integrity sha512-g8GWBkJLiIDRJfRXEdrd1wMXpNyGId2DkbfuwFahSb4OCvn717hyRJtAcEDISfp3zkwEhZ4Y4woHPA6DeyB3Fw==
+
 diffie-hellman@^5.0.0:
   version "5.0.3"
   resolved "https://registry.npmmirror.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -3765,6 +3889,21 @@ dir-glob@^3.0.1:
   dependencies:
     path-type "^4.0.0"
 
+disposables@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/disposables/-/disposables-1.0.2.tgz#36c6a674475f55a2d6913567a601444e487b4b6e"
+  integrity sha512-q1XTvs/XGdfubRSemB2+QRhJjIX4PerKkSom+i8Nkw3hCv6xISNrgaN442n2BunyBI4x77Om4ZAzSlqmhM9pwA==
+
+dnd-core@^2.5.4:
+  version "2.6.0"
+  resolved "https://registry.npmmirror.com/dnd-core/-/dnd-core-2.6.0.tgz#12bad66d58742c6e5f7cf2943fb6859440f809c4"
+  integrity sha512-5BfQHIp0XVd4ioF0q4GyUeHQQNCbqP+0SnUiP9TssoQ50wrP1NgSzDqZkjD5pFngsVz9txGin6rvTQD7w0qC3w==
+  dependencies:
+    asap "^2.0.6"
+    invariant "^2.0.0"
+    lodash "^4.2.0"
+    redux "^3.7.1"
+
 doctrine@^2.1.0:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -3791,6 +3930,13 @@ dom-converter@^0.2.0:
   dependencies:
     utila "~0.4"
 
+"dom-helpers@^2.4.0 || ^3.0.0":
+  version "3.4.0"
+  resolved "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+  integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+
 dom-serializer@^1.0.1:
   version "1.4.1"
   resolved "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -4447,6 +4593,11 @@ ext@^1.1.2:
   dependencies:
     type "^2.7.2"
 
+extend@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
 fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -4497,6 +4648,11 @@ fb-watchman@^2.0.0:
   dependencies:
     bser "2.1.1"
 
+fecha@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.npmmirror.com/fecha/-/fecha-2.3.1.tgz#921b4e5a9d331aaa9b65c1634b26fc45945f6421"
+  integrity sha512-syFl0GiovPbYLX0nozrL3UZXKTVXj0Ehu/Akr4S3LjyTKYjxj3nI4Mt/RCIiejEoiB0xX6fI8aTulA+PhurQNA==
+
 file-entry-cache@^6.0.1:
   version "6.0.1"
   resolved "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -4504,6 +4660,16 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
+file-saver@1.3.3:
+  version "1.3.3"
+  resolved "https://registry.npmmirror.com/file-saver/-/file-saver-1.3.3.tgz#cdd4c44d3aa264eac2f68ec165bc791c34af1232"
+  integrity sha512-2lGfU4gymmhXRUiPLeQlnlkMaSY8azJB9W8e/vFp44AlAOEvzf6XiBUoTHO9NBM4OVlehybxDM9B4SwLBh42mw==
+
+filesize@3.5.11:
+  version "3.5.11"
+  resolved "https://registry.npmmirror.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee"
+  integrity sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==
+
 fill-range@^7.0.1:
   version "7.0.1"
   resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -4587,6 +4753,15 @@ fork-ts-checker-webpack-plugin@7.2.4:
     semver "^7.3.5"
     tapable "^2.2.1"
 
+form-data@^2.3.1:
+  version "2.5.1"
+  resolved "https://registry.npmmirror.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
+  integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
 form-data@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@@ -4596,6 +4771,11 @@ form-data@^4.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+formidable@^1.2.0:
+  version "1.2.6"
+  resolved "https://registry.npmmirror.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
+  integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==
+
 fraction.js@^4.2.0:
   version "4.2.0"
   resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@@ -4918,6 +5098,16 @@ hmac-drbg@^1.0.1:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
+hoist-non-react-statics@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
+  integrity sha512-r8huvKK+m+VraiRipdZYc+U4XW43j6OFG/oIafe7GfDbRpCduRoX9JI/DRxqgtBSCeL+et6N6ibZoedHS2NyOQ==
+
+hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.1.1:
+  version "2.5.5"
+  resolved "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+  integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
+
 hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -5152,7 +5342,7 @@ intl@1.2.5:
   resolved "https://registry.npmmirror.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde"
   integrity sha512-rK0KcPHeBFBcqsErKSpvZnrOmWOj+EmDkyJ57e90YWaQNqbcivcqmKDlHEeNprDWOsKzPsh1BfSpPQdDvclHVw==
 
-invariant@^2.2.1, invariant@^2.2.4:
+invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -5733,6 +5923,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash-es@^4.2.1:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash.debounce@^4.0.8:
   version "4.0.8"
   resolved "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -5748,7 +5943,7 @@ lodash.merge@^4.6.2:
   resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash.throttle@^4.1.1:
+lodash.throttle@^4.0.1, lodash.throttle@^4.1.1:
   version "4.1.1"
   resolved "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
   integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
@@ -5763,7 +5958,7 @@ lodash.truncate@^4.4.2:
   resolved "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
   integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
 
-lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.20, lodash@^4.17.21:
+lodash@4.17.21, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.17.11, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.2.0, lodash@^4.2.1:
   version "4.17.21"
   resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -5778,7 +5973,7 @@ log-update@^4.0.0:
     slice-ansi "^4.0.0"
     wrap-ansi "^6.2.0"
 
-loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0:
   version "1.4.0"
   resolved "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -5899,6 +6094,11 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1:
   resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
+methods@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+  integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
 micromatch@^4.0.4, micromatch@^4.0.5:
   version "4.0.5"
   resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
@@ -5995,6 +6195,11 @@ ms@^2.1.1:
   resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
+nanoid@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-1.0.0.tgz#fd6c3d8c576ed6f7e6a45152c996ae3f5e7dfe36"
+  integrity sha512-eKG3dxtubAM8t+QgB7IAiBmSvE1AZ97JOUy35Xx1H66HzQY6EoWGgSMHckRal9uwsb4b5rzxmenAI7BLGCeB4g==
+
 nanoid@^3.1.32, nanoid@^3.3.4:
   version "3.3.4"
   resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@@ -6129,7 +6334,7 @@ nth-check@^2.0.1:
   dependencies:
     boolbase "^1.0.0"
 
-object-assign@4.x, object-assign@^4, object-assign@^4.1.1:
+object-assign@4.x, object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@@ -6413,6 +6618,11 @@ pbkdf2@^3.0.3:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
+
 picocolors@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -6839,7 +7049,7 @@ process@^0.11.10:
   resolved "https://registry.npmmirror.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
 
-prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@15.8.1, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.9, prop-types@^15.7.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -6900,6 +7110,13 @@ qrcode.react@^3.1.0:
   resolved "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
   integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
 
+qs@^6.5.1:
+  version "6.11.0"
+  resolved "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
+  integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+  dependencies:
+    side-channel "^1.0.4"
+
 query-string@^6.13.6:
   version "6.14.1"
   resolved "https://registry.npmmirror.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a"
@@ -6935,6 +7152,13 @@ quick-lru@^4.0.1:
   resolved "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
   integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
 
+raf@^3.2.0:
+  version "3.4.1"
+  resolved "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+  integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+  dependencies:
+    performance-now "^2.1.0"
+
 randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -6950,6 +7174,11 @@ randomfill@^1.0.3:
     randombytes "^2.0.5"
     safe-buffer "^5.1.0"
 
+range-parser@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+  integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
+
 rc-align@^4.0.0:
   version "4.0.15"
   resolved "https://registry.npmmirror.com/rc-align/-/rc-align-4.0.15.tgz#2bbd665cf85dfd0b0244c5a752b07565e9098577"
@@ -7429,6 +7658,56 @@ rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.13, rc-virtual-list@^3.4.8:
     rc-resize-observer "^1.0.0"
     rc-util "^5.15.0"
 
+react-click-outside@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/react-click-outside/-/react-click-outside-3.0.0.tgz#7a69a90d31b99204ef5d509cae91f52460d6fd69"
+  integrity sha512-UAtqurGBae+B+1Hu5NtCyJaBDHO74iMMZpBvEMs0Do3N9yjCU9WJOclTYdUXg773c1QjTtu5M2WCQILCVX1Vnw==
+  dependencies:
+    hoist-non-react-statics "^2.1.1"
+
+react-contextmenu@2.8.0:
+  version "2.8.0"
+  resolved "https://registry.npmmirror.com/react-contextmenu/-/react-contextmenu-2.8.0.tgz#b055477b9f2740a069ab001a9f7430935774cab9"
+  integrity sha512-yGmdwZCzndsP5bl1DkfrMi12eroG8N2pfettvrDEnAoIv8CG3yDfmR8wBTS6Iiu87sCRdy5iHgX97zQv+cWppQ==
+  dependencies:
+    classnames "^2.2.5"
+    object-assign "^4.1.0"
+
+react-display-name@^0.2.0:
+  version "0.2.5"
+  resolved "https://registry.npmmirror.com/react-display-name/-/react-display-name-0.2.5.tgz#304c7cbfb59ee40389d436e1a822c17fe27936c6"
+  integrity sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==
+
+react-dnd-html5-backend@2.5.4:
+  version "2.5.4"
+  resolved "https://registry.npmmirror.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz#974ad083f67b12d56977a5b171f5ffeb29d78352"
+  integrity sha512-jDqAkm/hI8Tl4HcsbhkBgB6HgpJR1e+ML1SbfxaegXYiuMxEVQm0FOwEH5WxUoo6fmIG4N+H0rSm59POuZOCaA==
+  dependencies:
+    lodash "^4.2.0"
+
+react-dnd-scrollzone@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/react-dnd-scrollzone/-/react-dnd-scrollzone-4.0.0.tgz#d707170c0cd3b7ab3d991dd6a8cc0b3712454139"
+  integrity sha512-yu9Z/K/7Fy4MtlGYp5eoaZY+Zz+wOccWdC98IeiMvkgoEP+YsC6UCjjMoZMJdeqpi8f1j3agPb4D5uB1OCwuKg==
+  dependencies:
+    hoist-non-react-statics "^1.2.0"
+    lodash.throttle "^4.0.1"
+    prop-types "^15.5.9"
+    raf "^3.2.0"
+    react-display-name "^0.2.0"
+
+react-dnd@2.5.4:
+  version "2.5.4"
+  resolved "https://registry.npmmirror.com/react-dnd/-/react-dnd-2.5.4.tgz#0b6dc5e9d0dfc2909f4f4fe736e5534f3afd1bd9"
+  integrity sha512-y9YmnusURc+3KPgvhYKvZ9oCucj51MSZWODyaeV0KFU0cquzA7dCD1g/OIYUKtNoZ+MXtacDngkdud2TklMSjw==
+  dependencies:
+    disposables "^1.0.1"
+    dnd-core "^2.5.4"
+    hoist-non-react-statics "^2.1.0"
+    invariant "^2.1.0"
+    lodash "^4.2.0"
+    prop-types "^15.5.10"
+
 react-dom@18.1.0:
   version "18.1.0"
   resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
@@ -7445,6 +7724,14 @@ react-draggable@^4.0.0, react-draggable@^4.0.3:
     clsx "^1.1.1"
     prop-types "^15.8.1"
 
+react-dropzone@4.2.1:
+  version "4.2.1"
+  resolved "https://registry.npmmirror.com/react-dropzone/-/react-dropzone-4.2.1.tgz#695e80bd0b065f1181e69f2d0f6d1d5cc72664c9"
+  integrity sha512-qKZRMYAU5tOAI/olSp8DNnmKtoG9PmU1g+Z+TR7/QV0q9rK/zLn7hVyFKU8BSJt9q+Z9umLKJrS9DuxyBiIDMA==
+  dependencies:
+    attr-accept "^1.0.3"
+    prop-types "^15.5.7"
+
 react-error-overlay@6.0.9:
   version "6.0.9"
   resolved "https://registry.npmmirror.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
@@ -7562,6 +7849,16 @@ react-router@6.3.0:
   dependencies:
     history "^5.2.0"
 
+react-sortable-hoc@0.6.8:
+  version "0.6.8"
+  resolved "https://registry.npmmirror.com/react-sortable-hoc/-/react-sortable-hoc-0.6.8.tgz#b08562f570d7c41f6e393fca52879d2ebb9118e9"
+  integrity sha512-sUUAtNdV84AKZ2o+F5lVOOFWcyWG6aGDkNFgHoieB1zFLeWLWENkix06asPS4/GhigfuRh06aZix1j3Qx8+NSQ==
+  dependencies:
+    babel-runtime "^6.11.6"
+    invariant "^2.2.1"
+    lodash "^4.12.0"
+    prop-types "^15.5.7"
+
 react-sortable-hoc@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7"
@@ -7571,6 +7868,17 @@ react-sortable-hoc@^2.0.0:
     invariant "^2.2.4"
     prop-types "^15.5.7"
 
+react-virtualized@9.12.0:
+  version "9.12.0"
+  resolved "https://registry.npmmirror.com/react-virtualized/-/react-virtualized-9.12.0.tgz#1b4d3e5ab197ed1d832df8e6688b1b18f725b6c5"
+  integrity sha512-QxqGUq7vktRsPnV/FclAGQ943rIYizTe9RIuyJGE75/3BA/7KJPXwXCQ9XW/3pnPKix+Jar0CCo3uTqoA4hFOg==
+  dependencies:
+    babel-runtime "^6.23.0"
+    classnames "^2.2.3"
+    dom-helpers "^2.4.0 || ^3.0.0"
+    loose-envify "^1.3.0"
+    prop-types "^15.5.4"
+
 react-zmage@^0.8.5-beta.37:
   version "0.8.5-beta.37"
   resolved "https://registry.npmmirror.com/react-zmage/-/react-zmage-0.8.5-beta.37.tgz#a310a2ea273685021e098edd10505011b3f3b778"
@@ -7611,7 +7919,7 @@ read-pkg@^5.2.0:
     parse-json "^5.0.0"
     type-fest "^0.6.0"
 
-readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6:
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6:
   version "2.3.7"
   resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
   integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -7658,6 +7966,16 @@ redux-saga@^0.16.0:
   resolved "https://registry.npmmirror.com/redux-saga/-/redux-saga-0.16.2.tgz#993662e86bc945d8509ac2b8daba3a8c615cc971"
   integrity sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w==
 
+redux@^3.7.1:
+  version "3.7.2"
+  resolved "https://registry.npmmirror.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
+  integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
+  dependencies:
+    lodash "^4.2.1"
+    lodash-es "^4.2.1"
+    loose-envify "^1.1.0"
+    symbol-observable "^1.0.3"
+
 redux@^4.2.0:
   version "4.2.0"
   resolved "https://registry.npmmirror.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
@@ -7701,6 +8019,11 @@ regenerator-runtime@0.13.9:
   resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
   integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
 
+regenerator-runtime@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+  integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
 regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
   version "0.13.11"
   resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
@@ -7903,6 +8226,13 @@ safe-stable-stringify@^2.1.0:
   resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+sanitize-filename@1.6.1:
+  version "1.6.1"
+  resolved "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a"
+  integrity sha512-XJty6Im+yPTLWiF7mW6BeZogNpYLk4jCSHJh1Xm8MyTcjajC1NDB/SwJEN5rDop3hp0AV2FFipwaTnmtKJMyRQ==
+  dependencies:
+    truncate-utf8-bytes "^1.0.0"
+
 sax@^1.2.4:
   version "1.2.4"
   resolved "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -8414,6 +8744,22 @@ stylis@^4.0.13, stylis@^4.1.3:
   resolved "https://registry.npmmirror.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
   integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
 
+superagent@3.8.3:
+  version "3.8.3"
+  resolved "https://registry.npmmirror.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
+  integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==
+  dependencies:
+    component-emitter "^1.2.0"
+    cookiejar "^2.1.0"
+    debug "^3.1.0"
+    extend "^3.0.0"
+    form-data "^2.3.1"
+    formidable "^1.2.0"
+    methods "^1.1.1"
+    mime "^1.4.1"
+    qs "^6.5.1"
+    readable-stream "^2.3.5"
+
 supports-color@^5.3.0:
   version "5.5.0"
   resolved "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -8486,6 +8832,11 @@ swr@^2.0.0:
   dependencies:
     use-sync-external-store "^1.2.0"
 
+symbol-observable@^1.0.3:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+  integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
 table@^6.8.0:
   version "6.8.1"
   resolved "https://registry.npmmirror.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf"
@@ -8592,6 +8943,13 @@ trim-newlines@^3.0.0:
   resolved "https://registry.npmmirror.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
   integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
+truncate-utf8-bytes@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
+  integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==
+  dependencies:
+    utf8-byte-length "^1.0.1"
+
 tslib@2.3.0:
   version "2.3.0"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
@@ -8795,6 +9153,11 @@ use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external
   resolved "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
 
+utf8-byte-length@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
+  integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==
+
 util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"

Some files were not shown because too many files changed in this diff