Browse Source

Merge branch 'develop' of xujunjie/WorkloadWeb into master

xujunjie 3 years ago
parent
commit
af30b3a87a
100 changed files with 9592 additions and 1962 deletions
  1. 12 11
      config/config.js
  2. 16 0
      config/router.config.js
  3. 4 2
      package.json
  4. 36 34
      public/luckysheet.html
  5. 49 0
      src/components/AuditForm/ComponentLibrary.js
  6. 92 0
      src/components/AuditForm/FormContent.js
  7. 326 0
      src/components/AuditForm/ItemAttribute.js
  8. 80 0
      src/components/AuditForm/constant.js
  9. 71 0
      src/components/AuditForm/index.js
  10. 384 0
      src/components/ChartUtils/utils.js
  11. 36 0
      src/components/DDComponents/DDAttachment/index.js
  12. 49 0
      src/components/DDComponents/DDCode/index.js
  13. 13 0
      src/components/DDComponents/DDDateField/index.js
  14. 22 0
      src/components/DDComponents/DDDateRangeField/index.js
  15. 30 0
      src/components/DDComponents/DDMultiSelectField/index.js
  16. 107 0
      src/components/DDComponents/DDPhotoField/AliyunOssUploader.js
  17. 46 0
      src/components/DDComponents/DDPhotoField/index.js
  18. 28 0
      src/components/DDComponents/DDSelectField/index.js
  19. 75 0
      src/components/DDComponents/DepartmentField/index.js
  20. 29 0
      src/components/DDComponents/InnerContactField/index.js
  21. 19 0
      src/components/DDComponents/NumberField/index.js
  22. 19 0
      src/components/DDComponents/PhoneField/index.js
  23. 90 0
      src/components/DDComponents/TableField/index.js
  24. 16 0
      src/components/DDComponents/TableField/index.less
  25. 124 0
      src/components/DDComponents/index.js
  26. 944 0
      src/components/DataMeter/config.js
  27. 233 0
      src/components/Flow/components/judgeComponent/index.tsx
  28. 76 0
      src/components/Flow/components/judgeModal/index.tsx
  29. 2 1
      src/components/Flow/config-toolbar.ts
  30. 109 5
      src/components/Flow/index.tsx
  31. 132 0
      src/components/Flow/node/auditNode/index.tsx
  32. 283 0
      src/components/Flow/node/auditNode/mapServe.tsx
  33. 3 1
      src/components/Flow/node/circle/index.tsx
  34. 2 0
      src/components/Flow/node/circle/mapServe.tsx
  35. 15 33
      src/components/Flow/node/control-map-service/components/edge.tsx
  36. 1 1
      src/components/Flow/node/fields/radio.tsx
  37. 113 0
      src/components/Flow/node/judgeNode/index.tsx
  38. 268 0
      src/components/Flow/node/judgeNode/mapServe.tsx
  39. 67 10
      src/components/Flow/node/rect/mapServe.tsx
  40. 21 3
      src/components/Flow/node/registerNode.tsx
  41. 43 43
      src/global.less
  42. 624 0
      src/models/dataMeterNew.js
  43. 80 5
      src/models/user.js
  44. 1 0
      src/models/xflow.js
  45. 64 0
      src/pages/DDLogin/index.js
  46. 0 0
      src/pages/Mobile/DataMeter/Alarm.js
  47. 118 0
      src/pages/Mobile/DataMeter/Chart.js
  48. 61 0
      src/pages/Mobile/DataMeter/Chart.less
  49. 39 0
      src/pages/Mobile/DataMeter/Craft.js
  50. 0 0
      src/pages/Mobile/DataMeter/Report.js
  51. 116 120
      src/pages/PurchaseAdmin/PurchaseList/Approval/ApprovalModal.js
  52. 77 71
      src/pages/PurchaseAdmin/PurchaseList/Approval/Auth.js
  53. 18 9
      src/pages/PurchaseAdmin/PurchaseList/Approval/AuthModal.js
  54. 102 24
      src/pages/PurchaseAdmin/PurchaseList/Approval/DetailModal.js
  55. 16 0
      src/pages/PurchaseAdmin/PurchaseList/Approval/DetailModal.less
  56. 35 74
      src/pages/PurchaseAdmin/PurchaseList/Approval/ExecutionModal.js
  57. 113 64
      src/pages/PurchaseAdmin/PurchaseList/Approval/List.js
  58. 33 81
      src/pages/PurchaseAdmin/PurchaseList/Approval/MemberModal.js
  59. 39 75
      src/pages/PurchaseAdmin/PurchaseList/Approval/QualityOperateModal.js
  60. 24 7
      src/pages/PurchaseAdmin/PurchaseList/Approval/models/approval.js
  61. 100 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/ApprovalProcess.tsx
  62. 130 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/AuditDetailed.js
  63. 271 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/AuditFlow.js
  64. 502 72
      src/pages/PurchaseAdmin/PurchaseList/Detail/CommitAuditModal.js
  65. 24 26
      src/pages/PurchaseAdmin/PurchaseList/Detail/ExportModal.js
  66. 82 65
      src/pages/PurchaseAdmin/PurchaseList/Detail/FilesModal.js
  67. 284 62
      src/pages/PurchaseAdmin/PurchaseList/Detail/FlowModal.js
  68. 112 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/HistoryDrawer.js
  69. 267 62
      src/pages/PurchaseAdmin/PurchaseList/Detail/Index.js
  70. 10 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/Index.less
  71. 24 15
      src/pages/PurchaseAdmin/PurchaseList/Detail/LuckySheet.js
  72. 8 4
      src/pages/PurchaseAdmin/PurchaseList/Detail/TimeNode.js
  73. 76 2
      src/pages/PurchaseAdmin/PurchaseList/Detail/models/detail.js
  74. 85 213
      src/pages/PurchaseAdmin/PurchaseList/Flow/Audit.js
  75. 8 11
      src/pages/PurchaseAdmin/PurchaseList/Flow/AuditList.js
  76. 3 0
      src/pages/PurchaseAdmin/PurchaseList/Flow/Flow.js
  77. 626 224
      src/pages/PurchaseAdmin/PurchaseList/Flow/FlowDetail.json
  78. 7 0
      src/pages/PurchaseAdmin/PurchaseList/Flow/List.js
  79. 168 0
      src/pages/PurchaseAdmin/PurchaseList/Flow/models/flow.js
  80. 32 18
      src/pages/PurchaseAdmin/PurchaseList/Index.js
  81. 104 6
      src/pages/PurchaseAdmin/PurchaseList/List/NewList.js
  82. 16 4
      src/pages/PurchaseAdmin/PurchaseList/List/models/list.js
  83. 46 3
      src/pages/PurchaseAdmin/PurchaseList/List/models/newList.js
  84. 176 0
      src/pages/PurchaseAdmin/PurchaseList/Report/DepCompareModal.js
  85. 31 41
      src/pages/PurchaseAdmin/PurchaseList/Report/Department.js
  86. 122 0
      src/pages/PurchaseAdmin/PurchaseList/Report/Finance.js
  87. 137 112
      src/pages/PurchaseAdmin/PurchaseList/Report/Project.js
  88. 1 3
      src/pages/PurchaseAdmin/PurchaseList/Report/Resource.js
  89. 55 21
      src/pages/PurchaseAdmin/PurchaseList/Report/UserProjectRptModal.js
  90. 2 2
      src/pages/PurchaseAdmin/PurchaseList/Report/UserRptModal.js
  91. 149 54
      src/pages/PurchaseAdmin/PurchaseList/Report/models/report.js
  92. 101 94
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/AddModal.js
  93. 59 14
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/Auth.js
  94. 32 52
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/AuthWorkList.js
  95. 13 26
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/CalendarModal.js
  96. 9 10
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/RejectModal.js
  97. 1 3
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/SearchForm.js
  98. 30 16
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/WorkList.js
  99. 18 32
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/index.js
  100. 26 21
      src/pages/PurchaseAdmin/PurchaseList/WorkingHours/models/workingHours.js

+ 12 - 11
config/config.js

@@ -29,20 +29,20 @@ const plugins = [
       },
       pwa: pwa
         ? {
-          workboxPluginMode: 'InjectManifest',
-          workboxOptions: {
-            importWorkboxFrom: 'local',
-          },
-        }
+            workboxPluginMode: 'InjectManifest',
+            workboxOptions: {
+              importWorkboxFrom: 'local',
+            },
+          }
         : {},
       ...(!TEST && os.platform() === 'darwin'
         ? {
-          dll: {
-            include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
-            exclude: ['@babel/runtime'],
-          },
-          hardSource: false,
-        }
+            dll: {
+              include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
+              exclude: ['@babel/runtime'],
+            },
+            hardSource: false,
+          }
         : {}),
     },
   ],
@@ -140,6 +140,7 @@ export default {
   chainWebpack: webpackPlugin,
   proxy: {
     '/api': {
+      // target: 'http://192.168.20.152:8888/',
       target: 'http://47.96.12.136:8896/',
       // target: 'http://oraysmart.com:8889/',
       // target: 'http://oraysmart.com:8888/api',

+ 16 - 0
config/router.config.js

@@ -7,6 +7,10 @@ export default [
         path: '/login',
         component: './PurchaseAdmin/PurchaseList/Login/Login',
       },
+      {
+        path: '/mobile/craft',
+        component: './Mobile/DataMeter/Craft',
+      },
       {
         path: '/home',
         component: './PurchaseAdmin/PurchaseList/Index',
@@ -40,6 +44,10 @@ export default [
             path: '/home/report/department',
             component: './PurchaseAdmin/PurchaseList/Report/Department',
           },
+          {
+            path: '/home/report/finance',
+            component: './PurchaseAdmin/PurchaseList/Report/Finance',
+          },
           {
             path: '/home/approval/list',
             component: './PurchaseAdmin/PurchaseList/Approval/List',
@@ -66,6 +74,14 @@ export default [
           },
         ],
       },
+      {
+        path: '/dd-login/:dingUserId',
+        component: './DDLogin/index',
+      },
+      {
+        path: '/mobiletest/chart',
+        component: './Mobile/DataMeter/Chart',
+      },
       {
         component: '404',
       },

+ 4 - 2
package.json

@@ -51,6 +51,7 @@
     "caniuse-lite": "^1.0.30001165",
     "classnames": "^2.2.6",
     "compression-webpack-plugin": "6.0.0",
+    "dingtalk-jsapi": "^2.15.4",
     "dva": "^2.4.1",
     "enquire-js": "^0.2.1",
     "exceljs": "^4.3.0",
@@ -74,6 +75,7 @@
     "omit.js": "^1.0.0",
     "path-to-regexp": "^3.0.0",
     "prop-types": "^15.6.2",
+    "qrcode.react": "^3.1.0",
     "qs": "^6.6.0",
     "range-parser": "1.2.0",
     "rc-animate": "^2.6.0",
@@ -136,10 +138,10 @@
     "tslint": "^5.12.1",
     "tslint-config-prettier": "^1.17.0",
     "tslint-react": "^3.6.0",
+    "typescript": "^4.4.4",
     "umi": "^2.4.4",
     "umi-plugin-ga": "^1.1.3",
-    "umi-plugin-react": "^1.4.2",
-    "typescript": "^4.4.4"
+    "umi-plugin-react": "^1.4.2"
   },
   "optionalDependencies": {
     "puppeteer": "^1.12.1"

+ 36 - 34
public/luckysheet.html

@@ -1,38 +1,40 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Document</title>
 
-   <!-- 线上发布使用路径 -->
-   <!-- <link rel="stylesheet" href="/bom/Luckysheet/plugins/css/pluginsCss.css" />
-   <link rel="stylesheet" href="/bom/Luckysheet/plugins/plugins.css" />
-   <link rel="stylesheet" href="/bom/Luckysheet/css/luckysheet.css" />
-   <link rel="stylesheet" href="/bom/Luckysheet/assets/iconfont/iconfont.css" />
-   <script src="/bom/Luckysheet/plugins/js/plugin.js"></script>
-   <script src="/bom/Luckysheet/luckysheet.umd.js"></script> -->
+<head>
+  <meta charset="UTF-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
 
-   <!-- 本地开发使用路径 -->
-   <link rel='stylesheet' href='http://localhost:3000/plugins/css/pluginsCss.css' />
-   <link rel='stylesheet' href='http://localhost:3000/plugins/plugins.css' />
-   <link rel='stylesheet' href='http://localhost:3000/css/luckysheet.css' />
-   <link rel='stylesheet' href='http://localhost:3000/assets/iconfont/iconfont.css' />
-   <script src="http://localhost:3000/plugins/js/plugin.js"></script>
-   <script src="http://localhost:3000/luckysheet.umd.js"></script>
-  </head>
-  <body>
-    <div
-      id="luckysheet"
-      style="margin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;"
-    ></div>
-  </body>
-  <script>
-    window.render = options => {
-      options = options || {};
-      options.container = 'luckysheet';
-      luckysheet.create(options);
-    };
-  </script>
-</html>
+  <!-- 线上发布使用路径 -->
+  <link rel="stylesheet" href="/bom/Luckysheet/plugins/css/pluginsCss.css" />
+  <link rel="stylesheet" href="/bom/Luckysheet/plugins/plugins.css" />
+  <link rel="stylesheet" href="/bom/Luckysheet/css/luckysheet.css" />
+  <link rel="stylesheet" href="/bom/Luckysheet/assets/iconfont/iconfont.css" />
+  <script src="/bom/Luckysheet/plugins/js/plugin.js"></script>
+  <script src="/bom/Luckysheet/luckysheet.umd.js"></script>
+
+  <!-- 本地开发使用路径 -->
+
+  <link rel='stylesheet' href='http://localhost:3000/plugins/css/pluginsCss.css' />
+  <link rel='stylesheet' href='http://localhost:3000/plugins/plugins.css' />
+  <link rel='stylesheet' href='http://localhost:3000/css/luckysheet.css' />
+  <link rel='stylesheet' href='http://localhost:3000/assets/iconfont/iconfont.css' />
+  <script src="http://localhost:3000/plugins/js/plugin.js"></script>
+  <script src="http://localhost:3000/luckysheet.umd.js"></script>
+</head>
+
+<body>
+  <div id="luckysheet" style="margin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;">
+  </div>
+</body>
+<script>
+  window.render = options => {
+    options = options || {};
+    options.container = 'luckysheet';
+    luckysheet.create(options);
+  };
+</script>
+
+</html>

+ 49 - 0
src/components/AuditForm/ComponentLibrary.js

@@ -0,0 +1,49 @@
+import { message, Col, Modal, Row } from 'antd';
+import React, { useState } from 'react';
+import { COMPONENT_LIST } from './constant';
+
+function ComponentLibrary(props) {
+  const { visible, onCancel, onOk } = props;
+  const [currnetItem, setCurrentItem] = useState(null);
+
+  const handleOk = () => {
+    if (!currnetItem) {
+      message.error('请选择控件');
+      return;
+    }
+    setCurrentItem(null);
+    onOk?.(currnetItem);
+  };
+  const handleCancel = () => {
+    setCurrentItem(null);
+    onCancel?.();
+  };
+  return (
+    <Modal visible={visible} onCancel={handleCancel} onOk={handleOk}>
+      <Row gutter={12} style={{ paddingTop: 20 }}>
+        {COMPONENT_LIST.map(item => (
+          <Col span={8}>
+            <div
+              onClick={() => setCurrentItem(item)}
+              style={{
+                display: 'flex',
+                justifyContent: 'flex-start',
+                alignItems: 'center',
+                border: item == currnetItem ? '1px solid #1890FF' : '1px solid #aaa',
+                width: '100%',
+                padding: '4px 12px',
+                cursor: 'pointer',
+                margin: '10px 0',
+              }}
+            >
+              {item.icon}
+              <span style={{ marginLeft: 8 }}>{item.props.label}</span>
+            </div>
+          </Col>
+        ))}
+      </Row>
+    </Modal>
+  );
+}
+
+export default ComponentLibrary;

+ 92 - 0
src/components/AuditForm/FormContent.js

@@ -0,0 +1,92 @@
+import { Form } from 'antd';
+import React, { useState } from 'react';
+import { ArrowUpOutlined, ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
+
+function FormContent(props) {
+  const { list, onChange, onSelect } = props;
+  const [currentItem, setCurrentItem] = useState(null);
+  const handleDelete = index => {
+    let _list = [...list];
+    _list.splice(index, 1);
+    onChange(_list);
+  };
+  const handleUp = index => {
+    let _list = [...list];
+    // 跟上一位置换位置
+    const temp = _list[index - 1];
+    _list[index - 1] = _list[index];
+    _list[index] = temp;
+    onChange(_list);
+  };
+  const handleDown = index => {
+    let _list = [...list];
+    const temp = _list[index + 1];
+    _list[index + 1] = _list[index];
+    _list[index] = temp;
+    onChange(_list);
+  };
+  const handleSelect = index => {
+    setCurrentItem(index);
+    onSelect(index);
+  };
+  return (
+    <div style={{ width: 300 }}>
+      {list.map((item, index) => {
+        const btns = (
+          <>
+            {index != 0 && (
+              <ArrowUpOutlined
+                style={{ marginLeft: 5, cursor: 'pointer' }}
+                onClick={() => handleUp(index)}
+              />
+            )}
+            {index != list.length - 1 && (
+              <ArrowDownOutlined
+                style={{ marginLeft: 5, cursor: 'pointer' }}
+                onClick={() => handleDown(index)}
+              />
+            )}
+            <DeleteOutlined
+              style={{ marginLeft: 5, cursor: 'pointer' }}
+              onClick={() => handleDelete(index)}
+            />
+          </>
+        );
+        return (
+          <FormItem
+            key={item.id}
+            active={index == currentItem}
+            onClick={() => handleSelect(index)}
+            item={item}
+            btns={btns}
+          />
+        );
+      })}
+    </div>
+  );
+}
+
+function FormItem(props) {
+  const { item, btns, active, onClick } = props;
+  const { label, placeholder, required } = item.props;
+  return (
+    <div
+      style={{
+        marginBottom: 20,
+        padding: '4px 12px',
+        border: '1px solid #666',
+        borderLeft: active ? '10px solid #1890FF' : '1px solid #666',
+      }}
+      onClick={onClick}
+    >
+      <div style={{ fontSzie: 24, color: '#000', fontWeight: 'bold', position: 'relative' }}>
+        {required && <i style={{ color: 'red' }}>*</i>}
+        {label}
+        <div style={{ position: 'absolute', right: 0, top: 0, padding: '5px 10px' }}>{btns}</div>
+      </div>
+      <div style={{ color: '#999', fontSize: 16 }}>{placeholder}</div>
+    </div>
+  );
+}
+
+export default FormContent;

+ 326 - 0
src/components/AuditForm/ItemAttribute.js

@@ -0,0 +1,326 @@
+import { Form, Button, Switch, Input, Radio, Space, Row } from 'antd';
+import React, { useMemo, useState, useEffect } from 'react';
+import { DeleteOutlined } from '@ant-design/icons';
+function ItemAttribute(props) {
+  const { item, onChange } = props;
+
+  const renderForm = () => {
+    let FormContent;
+    const formProps = {
+      btns: <Form.Item>
+        <Button type="primary" htmlType="submit" style={{ marginRight: 20 }}>
+          保存
+        </Button>
+      </Form.Item>,
+      item,
+      onFinish: values => {
+        console.log(values);
+        onChange?.(values);
+      }
+    }
+    switch (item.componentName) {
+      case 'InnerContactField':
+        FormContent = <InnerContactField {...formProps} />;
+        break;
+      case 'DepartmentField':
+        FormContent = <DepartmentField {...formProps} />;
+        break;
+      case 'TextField':
+        FormContent = <TextField {...formProps} />;
+        break;
+      case 'TextareaField':
+        FormContent = <TextareaField {...formProps} />;
+        break;
+      case 'DDSelectField':
+        FormContent = <DDSelectField {...formProps} />;
+        break;
+      case 'DDMultiSelectField':
+        FormContent = <DDMultiSelectField {...formProps} />;
+        break;
+      case 'NumberField':
+        FormContent = <NumberField {...formProps} />;
+        break;
+    }
+
+    return FormContent;
+  };
+
+  if (!item) return null;
+
+  return (
+    <div
+      style={{
+        // position: 'absolute',
+        // top: 0,
+        // right: 0,
+        width: 300,
+        height: '100%',
+        overflowY: 'auto',
+      }}
+    >
+      {renderForm()}
+    </div>
+  );
+}
+
+export default ItemAttribute;
+
+function InnerContactField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  const onSwitchChange = checked => {
+    form.setFieldValue("choice", checked ? 1 : 0);
+  }
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="choice">
+        <Radio.Group>
+          <Space direction="vertical">
+            <Radio value={'0'}>只能选择一人</Radio>
+            <Radio value={'1'}>可同时选择多人</Radio>
+          </Space>
+        </Radio.Group>
+      </Form.Item>
+      <Form.Item label="必填" name="required" valuePropName="checked">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function DepartmentField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="choice">
+        <Radio.Group>
+          <Space direction="vertical">
+            <Radio value={'0'}>只能选择一人</Radio>
+            <Radio value={'1'}>可同时选择多人</Radio>
+          </Space>
+        </Radio.Group>
+      </Form.Item>
+      <Form.Item label="必填" name="required" valuePropName="checked">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function TextField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+
+  const onReset = () => {
+    form.resetFields();
+  };
+  return (
+    <Form
+      form={form}
+      labelCol={{ span: 8 }}
+      wrapperCol={{ span: 16 }}
+      onFinish={onFinish}
+      autoComplete="off"
+      initialValues={item.props}
+    >
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function TextareaField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function DDSelectField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  const handleFinish = value => {
+    let tempValue = { ...value };
+    let arr = [];
+    tempValue.options.map(item => {
+      arr.push(item.value);
+    })
+    if (arr) {
+      arr = arr.filter(item => item)
+      arr = [...new Set(arr)];
+      tempValue.options = arr
+    }
+    onFinish?.(tempValue);
+  }
+
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={handleFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="options" wrapperCol={{ span: 24 }}>
+        <SelectItem />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function DDMultiSelectField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  const handleFinish = value => {
+    let tempValue = { ...value };
+    let arr = [];
+    tempValue.options.map(item => {
+      arr.push(item.value);
+    })
+    if (arr) {
+      arr = arr.filter(item => item)
+      arr = [...new Set(arr)];
+      tempValue.options = arr
+    }
+    onFinish?.(tempValue);
+  }
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={handleFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="options" wrapperCol={{ span: 24 }}>
+        <SelectItem />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+
+function SelectItem(props) {
+  const { value, onChange } = props;
+  const [localValue, setLocalValue] = useState([]);
+  const pushItem = item => {
+    let tempValue = [...localValue, {
+      id: +new Date(),
+      value: item
+    }];
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }
+
+  const handleDelete = index => {
+    let tempValue = [...localValue];
+    tempValue.splice(index, 1);
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }
+  const handleInputOnChange = (targetValue, index) => {
+    let tempValue = [...localValue];
+    tempValue[index].value = targetValue;
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }
+  useEffect(() => {
+    let tempValue = value.map(item => ({ id: +new Date(), value: item }));
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }, []);
+
+  return (
+    <div style={{
+      minHeight: 40,
+      display: 'flex',
+      flexDirection: 'column'
+    }}>
+      <div>
+        <div style={{
+          fontSize: 4,
+          color: '#40a9ff',
+          cursor: 'pointer',
+          lineHeight: '32px'
+        }} onClick={() => { pushItem('') }}>添加选项</div>
+      </div>
+      <div style={{ minHeight: 20 }}>
+        {
+          localValue.map((item, index) =>
+            <div style={{
+              display: 'flex',
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 5
+            }}
+              key={item.id}
+            >
+              <Input style={{ marginRight: 10 }} value={item.value} onChange={e => handleInputOnChange(e.target.value, index)} />
+              <DeleteOutlined
+                onClick={() => handleDelete(index)}
+              />
+            </div>)
+        }
+      </div>
+    </div >
+  )
+}
+
+function NumberField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="单位" name="unit">
+        <Input />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}

+ 80 - 0
src/components/AuditForm/constant.js

@@ -0,0 +1,80 @@
+import {
+  UserOutlined,
+  TeamOutlined,
+  ItalicOutlined,
+  FontSizeOutlined,
+  BorderOutlined,
+  BlockOutlined,
+  FieldNumberOutlined,
+} from '@ant-design/icons';
+
+export const COMPONENT_LIST = [
+  {
+    componentName: 'InnerContactField',
+    icon: <UserOutlined />,
+    props: {
+      label: '联系人',
+      placeholder: '请选择联系人',
+      required: false,
+      choice: '0'
+    },
+  },
+  {
+    componentName: 'DepartmentField',
+    icon: <TeamOutlined />,
+    props: {
+      label: '选择部门',
+      placeholder: '请选择部门',
+      required: false,
+      choice: '0'
+    },
+  },
+  {
+    componentName: 'TextField',
+    icon: <ItalicOutlined />,
+    props: {
+      label: '单行输入框',
+      placeholder: '请输入',
+      required: false,
+    },
+  },
+  {
+    componentName: 'TextareaField',
+    icon: <FontSizeOutlined />,
+    props: {
+      label: '多行输入框',
+      placeholder: '请输入',
+      required: false,
+    },
+  },
+  {
+    componentName: 'DDSelectField',
+    icon: <BorderOutlined />,
+    props: {
+      label: '单选框',
+      placeholder: '请选择',
+      required: false,
+      options: [],
+    },
+  },
+  {
+    componentName: 'DDMultiSelectField',
+    icon: <BlockOutlined />,
+    props: {
+      label: '多选框',
+      placeholder: '请选择',
+      require: false,
+      options: [],
+    },
+  },
+  {
+    componentName: 'NumberField',
+    icon: <FieldNumberOutlined />,
+    props: {
+      label: '数字输入框',
+      placeholder: '请输入',
+      required: false,
+      unit: ''
+    },
+  },
+];

+ 71 - 0
src/components/AuditForm/index.js

@@ -0,0 +1,71 @@
+import React, { useState, useEffect } from 'react';
+import FormContent from './FormContent';
+import ComponentLibrary from './ComponentLibrary';
+import ItemAttribute from './ItemAttribute';
+import { uuidv4 } from '@antv/xflow';
+import { Button } from 'antd';
+
+function AuditForm(props) {
+  const { value, onChange } = props;
+  const [formList, setFormList] = useState([]);
+  const [select, setSelect] = useState(-1);
+  const [visible, setVisible] = useState(false);
+
+  const handleAddItem = item => {
+    const formItem = generateItem(item);
+    handleChangeList([...formList, formItem]);
+    setVisible(false);
+  };
+
+  const generateItem = item => {
+    let newItem = {
+      ...item,
+      props: { ...item.props, id: `${item.componentName}_${uuidv4()}` },
+    };
+    delete newItem.icon;
+    return newItem;
+  };
+
+  const onChangeAttribute = newItem => {
+    let oldValue = formList[select].props;
+    formList[select].props = { ...oldValue, ...newItem };
+    handleChangeList([...formList]);
+  };
+
+  const handleChangeList = list => {
+    setFormList(list);
+    onChange?.(list);
+  };
+
+  useEffect(() => {
+    if (value instanceof Array) {
+      setFormList([...value]);
+      onChange?.([...value]);
+    }
+  }, [value]);
+
+  return (
+    <div>
+      <Button onClick={() => setVisible(true)}>增加控件</Button>
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'flex-start',
+          width: 800,
+          marginTop: 20,
+        }}
+      >
+        <FormContent onSelect={setSelect} onChange={handleChangeList} list={formList}></FormContent>
+        <ItemAttribute item={formList[select]} onChange={onChangeAttribute}></ItemAttribute>
+      </div>
+      <ComponentLibrary
+        onOk={handleAddItem}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+      ></ComponentLibrary>
+    </div>
+  );
+}
+
+export default AuditForm;

+ 384 - 0
src/components/ChartUtils/utils.js

@@ -0,0 +1,384 @@
+import moment from 'moment';
+
+import {
+  getDeviceRealData,
+  getDeviceRealDataByTime,
+  queryFormCurrentData,
+  queryFormHistoryData,
+} from '@/services/ProjectAdmin';
+
+const computePrefixExpression = (function() {
+  var s1 = [],
+    result;
+  return function(prefixExpression) {
+    s1.length = 0;
+    //计算
+    while (prefixExpression.length > 0) {
+      var o = prefixExpression.shift();
+      if (operator_map.indexOf(o) == -1) {
+        s1.push(Number(o));
+      } else {
+        switch (o) {
+          case '+': {
+            result = s1.pop() + s1.pop();
+            break;
+          }
+          case '-': {
+            result = s1.pop() - s1.pop();
+            break;
+          }
+          case '*': {
+            result = s1.pop() * s1.pop();
+            break;
+          }
+          case '/': {
+            result = s1.pop() / s1.pop();
+            break;
+          }
+        }
+        s1.push(result);
+      }
+    }
+    return s1[0];
+  };
+})();
+
+export async function getOptions(values, datas, formula, projectId) {
+  let allDatas = getAllParams(datas, formula);
+
+  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 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 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);
+  if (values.timeType) {
+    // 获得时间轴全集
+    let times = getTimes(plcData[0]?.data, formData[0]?.data);
+
+    // 根据时间全集补填数据
+    formatData(plcData, times);
+    formatData(formData, times);
+  }
+  return {
+    plcData,
+    formData,
+  };
+}
+
+// 根据时间进行格式化数据
+function formatData(datas, times) {
+  datas.forEach(item => {
+    // 默认数据
+    let newItemData = [];
+    let i = 0;
+    times.forEach(t => {
+      if (item.data[i]?.htime != t) {
+        // 空缺时间要补0
+        newItemData.push({
+          htime: t,
+          val: 0,
+        });
+      } else {
+        newItemData.push(item.data[i]);
+        i++;
+      }
+    });
+    // 使用新值
+    item.data = newItemData;
+  });
+}
+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));
+}
+async function getFormData(values, datas, projectId) {
+  let arrtData = values.data || [];
+  const params = getFormParams(datas, projectId);
+  if (!values.timeType) {
+    // 请求最新数据
+    return await getFormCurrentData(params, values);
+  } else {
+    // 请求历史数据
+    return await getFormHistoryData(params, values);
+  }
+}
+// 请求表单最新数据
+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 } = 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:ss');
+    sTime = moment(date[0])
+      .set(clear)
+      .format('YYYY-MM-DD HH:mm:ss');
+  } else {
+    let currentDate = moment();
+    eTime = currentDate.format('YYYY-MM-DD HH:mm:ss');
+    sTime = currentDate.add(-1 * timeType, 'hour').format('YYYY-MM-DD HH:mm:ss');
+  }
+
+  for (let i = 0; i < params.length; i++) {
+    const resData = await queryFormHistoryData({ ...params[i], eTime, sTime });
+    data = [...data, ...resData];
+  }
+  return data;
+}
+async function getPlcData(values, datas) {
+  let arrtData = values.data || [];
+  let params = getSingleData(datas);
+  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 || []).map(item => {
+          return {
+            val: Number(item.val),
+            htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
+          };
+        }),
+      });
+    }
+    return singleData;
+  }
+}
+
+// 根据数据项获取请求参数
+function getSingleData(datas) {
+  let params = [];
+  datas.forEach(({ device_id, device_items, seq }) => {
+    if (device_id && device_items) {
+      params.push({
+        deviceName: seq,
+        deviceId: device_id,
+        deviceItems: device_items,
+      });
+    }
+  });
+  return params;
+}
+function findPlcData(paramsInfo, plcData, timeType) {
+  // 实时数据与历史数据结构不一致  需判断
+  if (timeType) {
+    return plcData.find(resItem => resItem.paramsInfo.deviceName == paramsInfo.seq);
+  } else {
+    let res = plcData.find(resItem => resItem.alias == paramsInfo.seq);
+    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;
+}
+
+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);
+    } else {
+      if (timeType != -1) {
+        let currentDate = moment();
+        etime = currentDate * 1;
+        stime = currentDate.add(-1 * timeType, 'hour') * 1;
+      }
+      DATA_CACHE[key] = await getDeviceRealDataByTime({
+        deviceid: params.deviceId * 1,
+        dataitemid: params.deviceItems,
+        stime,
+        etime,
+        size,
+        interval,
+        aggregator,
+      });
+    }
+  }
+  return DATA_CACHE[key];
+}
+
+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,
+    };
+  }
+}

+ 36 - 0
src/components/DDComponents/DDAttachment/index.js

@@ -0,0 +1,36 @@
+import React, { useState, useEffect } from 'react';
+import { Upload, Button, message } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+
+const CORP_ID = 'ding0cdce2d5dbf986d9';
+const AGENT_ID = '1788653353';
+
+function DDAttachment(props) {
+  const { disabled, onChange, spaceId = '213865445' } = props;
+  const [value, setValue] = useState([]);
+
+  const upload = () => {
+    // dd.biz.util.uploadAttachment({
+    //   // image: { multiple: true, compress: false, max: 9, spaceId: '12345' },
+    //   space: { corpId: CORP_ID, spaceId: spaceId, isCopy: 1, max: 9 },
+    //   file: { spaceId: spaceId, max: 1 },
+    //   types: ['file', 'space'], //PC端支持["photo","file","space"]
+    //   onSuccess: function(result) {
+    //     //onSuccess将在文件上传成功之后调用
+    //     onChange?.(result.data);
+    //   },
+    //   onFail: function(err) {
+    //     console.log(err);
+    //     message.error('附件上传失败');
+    //   },
+    // });
+  };
+
+  return (
+    <div onClick={upload}>
+      <Button icon={<PlusOutlined />}>添加附件</Button>
+    </div>
+  );
+}
+
+export default DDAttachment;

+ 49 - 0
src/components/DDComponents/DDCode/index.js

@@ -0,0 +1,49 @@
+import QRCode from 'qrcode.react';
+import React, { useEffect, useState } from 'react';
+import { Spin } from 'antd';
+import { connect } from 'dva';
+
+let timer;
+function DDCode(props) {
+  const {
+    value,
+    onChange,
+    currentUser: { DingUserId },
+  } = props;
+  const [loading, setLoading] = useState(false);
+  // const [time, setTime] = useState(0);
+
+  // const queryCode = async () => {
+  //   setLoading(true);
+  //   // TODO 查询接口获得code
+
+  //   setLoading(false);
+  //   clearInterval(timer);
+  //   onChange({
+  //     code: '',
+  //   });
+  //   let second = 60 * 5;
+  //   if (second <= 0) return;
+  //   setTime(second);
+
+  //   timer = setInterval(() => {
+  //     setTime(time => {
+  //       if (time > 0) {
+  //         return time--;
+  //       }
+  //       return time;
+  //     });
+  //   }, second * 1000);
+  // };
+  if (!DingUserId) return <div>未知用户,请联系管理员。</div>;
+  return (
+    <div>
+      <Spin spinning={loading}>
+        <QRCode size={128} value={`http://47.96.12.136:8896/#/bom/dd-login/${DingUserId}`} />
+        请使用钉钉扫码授权
+      </Spin>
+    </div>
+  );
+}
+
+export default connect(({ user }) => ({ currentUser: user.currentUser }))(DDCode);

+ 13 - 0
src/components/DDComponents/DDDateField/index.js

@@ -0,0 +1,13 @@
+import React from 'react';
+import { DatePicker } from 'antd';
+
+function DDDateField(props) {
+  const { format, disabled, onChange } = props;
+
+  const handleChange = date => {
+    onChange?.(date.format('YYYY-MM-DD'));
+  };
+  return <DatePicker disabled={disabled} format={format.replace("yyyy","YYYY").replace("dd","DD")} onChange={handleChange} />;
+}
+
+export default DDDateField;

+ 22 - 0
src/components/DDComponents/DDDateRangeField/index.js

@@ -0,0 +1,22 @@
+import React from 'react';
+import { DatePicker } from 'antd';
+
+const { RangePicker } = DatePicker;
+
+function DDDateRangeField(props) {
+  const { format, disabled, onChange } = props;
+
+  const handleChange = date => {
+    let value = [date[0].format('YYYY-MM-DD'), date[1].format('YYYY-MM-DD')];
+    onChange?.(JSON.stringify(value));
+  };
+  return (
+    <RangePicker
+      disabled={disabled}
+      format={format.replace('yyyy', 'YYYY').replace('dd', 'DD')}
+      onChange={handleChange}
+    />
+  );
+}
+
+export default DDDateRangeField;

+ 30 - 0
src/components/DDComponents/DDMultiSelectField/index.js

@@ -0,0 +1,30 @@
+import React from 'react';
+import { Select } from 'antd';
+
+const { Option } = Select;
+
+function DDMultiSelectField(props) {
+  const { options, disabled, onChange } = props;
+
+  return (
+    <Select
+      mode="multiple"
+      allowClear
+      style={{ width: '100%' }}
+      disabled={disabled}
+      onChange={value => {
+        onChange(value);
+      }}
+    >
+      {options?.map(cur => {
+        return (
+          <Option key={cur} value={cur}>
+            {cur}
+          </Option>
+        );
+      })}
+    </Select>
+  );
+}
+
+export default DDMultiSelectField;

+ 107 - 0
src/components/DDComponents/DDPhotoField/AliyunOssUploader.js

@@ -0,0 +1,107 @@
+import React from 'react'
+import { Upload, message, Button} from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+
+class AliyunOSSUpload extends React.Component {
+  state = {
+    OSSData: {},
+  };
+
+  componentDidMount() {
+    this.init();
+  }
+
+  init = () => {
+    try {
+      const { OSSData } = this.props;
+
+      this.setState({
+        OSSData,
+      });
+    } catch (error) {
+      message.error(error);
+    }
+  };
+
+  onChange = ({ file, fileList }) => {
+    const { onChange, onDone , onUploading} = this.props;
+    console.log('Aliyun OSS:', file, fileList);
+    if (onChange) {
+      onChange([...fileList]);
+    }
+
+    if (onDone) {
+      if (file.status === 'done')
+        onDone(file)
+    }
+    if(onUploading && file.status === 'uploading') {
+      onUploading({ file, fileList })
+    }
+    this.setState({ fileList: [...fileList] });
+  };
+
+  onRemove = file => {
+    const { value, onChange } = this.props;
+
+    const files = value.filter(v => v.url !== file.url);
+
+    if (onChange) {
+      onChange(files);
+    }
+  };
+
+  transformFile = file => {
+    const { OSSData } = this.state;
+    file.url = OSSData.dir + file.name;
+    return file;
+  };
+
+  getExtraData = file => {
+    const { OSSData } = this.state;
+
+    return {
+      key: file.url,
+      OSSAccessKeyId: OSSData.accessid,
+      policy: OSSData.policy,
+      Signature: OSSData.signature,
+    };
+  };
+
+  beforeUpload = async () => {
+    const { OSSData } = this.state;
+    const expire = OSSData.expire * 1000;
+
+    if (expire < Date.now()) {
+      await this.init();
+    }
+    return true;
+  };
+
+  render() {
+    const { value, directory, label, noStyle, showUploadList, accept } = this.props;
+    const props = {
+      name: 'file',
+      fileList: this.state.fileList,
+      action: this.state.OSSData.host,
+      onChange: this.onChange,
+      onRemove: this.onRemove,
+      transformFile: this.transformFile,
+      data: this.getExtraData,
+      beforeUpload: this.beforeUpload,
+      accept: accept,
+      showUploadList: showUploadList !== false,
+      headers: { "Access-Control-Allow-Origin": "*" }
+    };
+    return (
+      <Upload {...props} directory={directory}>
+        {noStyle ? label : (<Button type="primary">
+          <PlusOutlined /> {label}
+        </Button>)}
+
+      </Upload>
+    );
+  }
+}
+
+
+export default AliyunOSSUpload;

+ 46 - 0
src/components/DDComponents/DDPhotoField/index.js

@@ -0,0 +1,46 @@
+import React, { useState, useEffect } from 'react';
+import { Upload, Button, Image } from 'antd';
+import AliyunOssUploader from '@/components/OssUpload/AliyunOssUploader';
+import { queryOSSData } from '@/services/boom';
+
+function DDPhotoField(props) {
+  const { disabled, onChange } = props;
+  const [value, setValue] = useState([]);
+  const [ossData, setOssData] = useState();
+
+  const OnModelFileDone = file => {
+    var path = ossData.host + '/' + file.url;
+    let newValue = [...value, path];
+    setValue(newValue);
+    onChange(JSON.stringify(newValue));
+  };
+  const OnUploading = file => {};
+
+  useEffect(() => {
+    queryOSSData().then(res => {
+      setOssData(res.data);
+    });
+  }, []);
+
+  return ossData ? (
+    <div>
+      <div>
+        {value.map(img => (
+          <Image height={200} src={img} />
+        ))}
+      </div>
+      <AliyunOssUploader
+        OSSData={ossData}
+        onDone={OnModelFileDone}
+        onUploading={OnUploading}
+        noStyle={false}
+        showUploadList={false}
+        directory={false}
+        accept={'.png, .jpg, .jpeg'}
+        label="上传图片"
+      ></AliyunOssUploader>
+    </div>
+  ) : null;
+}
+
+export default DDPhotoField;

+ 28 - 0
src/components/DDComponents/DDSelectField/index.js

@@ -0,0 +1,28 @@
+import React from 'react';
+import { Select } from 'antd';
+
+const { Option } = Select;
+
+function DDSelectField(props) {
+  const { options, disabled, onChange } = props;
+
+  return (
+    <Select
+      style={{ width: '100%' }}
+      disabled={disabled}
+      onChange={value => {
+        onChange(String(value));
+      }}
+    >
+      {options?.map(cur => {
+        return (
+          <Option key={cur} value={cur}>
+            {cur}
+          </Option>
+        );
+      })}
+    </Select>
+  );
+}
+
+export default DDSelectField;

+ 75 - 0
src/components/DDComponents/DepartmentField/index.js

@@ -0,0 +1,75 @@
+import { TreeSelect } from 'antd';
+import React, { useState, useEffect } from 'react';
+import { queryDDdepList } from '@/services/boom';
+import { connect } from 'dva';
+
+function DepartmentField(props) {
+  const { value, onChange, depUserTree } = props;
+
+  // const [value, setValue] = useState();
+  const [treeData, setTreeData] = useState([]);
+
+  const genTreeNode = dep => {
+    return {
+      id: dep.dept_id,
+      pId: dep.parent_id,
+      value: dep.dept_id,
+      title: dep.name,
+      isLeaf: false,
+    };
+  };
+
+  const onLoadData = async ({ id }) => {
+    let depList = await queryDDdepList({ dept_id: id });
+
+    console.log(depList);
+    if (depList.length > 0) {
+      let nodes = depList.map(genTreeNode);
+      setTreeData([...treeData, ...nodes]);
+    }
+  };
+
+  const onChangeValue = newValue => {
+    console.log(newValue);
+    let dep = depUserTree.find(dep => dep.id == newValue);
+    onChange(String(dep?.ID));
+    // onChange({ value: dep?.title, id: dep?.ID });
+  };
+
+  // useEffect(() => {
+  //   onLoadData({});
+  // }, []);
+
+  return (
+    // <TreeSelect
+    //   treeDataSimpleMode
+    //   style={{
+    //     width: '100%',
+    //   }}
+    //   // value={value}
+    //   dropdownStyle={{
+    //     maxHeight: 400,
+    //     overflow: 'auto',
+    //   }}
+    //   placeholder="请选择部门"
+    //   onChange={onChangeValue}
+    //   loadData={onLoadData}
+    //   treeData={treeData}
+    // />
+    <TreeSelect
+      showSearch
+      multiple
+      allowClear
+      dropdownStyle={{
+        maxHeight: 400,
+        overflow: 'auto',
+      }}
+      style={{ width: '100%' }}
+      placeholder="请选择部门"
+      treeData={depUserTree}
+      onChange={onChangeValue}
+    />
+  );
+}
+
+export default connect(({ user }) => ({ depUserTree: user.depUserTree }))(DepartmentField);

+ 29 - 0
src/components/DDComponents/InnerContactField/index.js

@@ -0,0 +1,29 @@
+import React from 'react';
+import { message, Select } from 'antd';
+import { connect } from 'dva';
+
+const { Option } = Select;
+
+function InnerContactField(props) {
+  const { value, userList, onChange } = props;
+  return (
+    <Select
+      showSearch
+      onChange={value => {
+        onChange(String(value));
+        // onChange(JSON.stringify([value]));
+      }}
+      filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())}
+    >
+      {(userList || []).map(item => (
+        <Option key={item.ID} value={item.ID}>
+          {item.CName}
+        </Option>
+      ))}
+    </Select>
+  );
+}
+
+export default connect(({ user }) => ({
+  userList: user.list,
+}))(InnerContactField);

+ 19 - 0
src/components/DDComponents/NumberField/index.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import { InputNumber } from 'antd';
+
+function NumberField(props) {
+  const { onChange, disabled, unit } = props;
+
+  return (
+    <InputNumber
+      style={{ width: '100%' }}
+      disabled={disabled}
+      formatter={value => `${value}${unit || ''}`}
+      onChange={e => {
+        onChange?.(String(e));
+      }}
+    />
+  );
+}
+
+export default NumberField;

+ 19 - 0
src/components/DDComponents/PhoneField/index.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import { Input, Row, Col, Select } from 'antd';
+import { connect } from 'dva';
+
+const { Option } = Select;
+
+function PhoneField(props) {
+  const { onChange } = props;
+  return (
+    <Input.Group size="large">
+      <Select defaultValue="+86">
+        <Option value="+86">+86</Option>
+      </Select>
+      <Input style={{ width: '80%' }} {...props} onChange={e => onChange(e.target.value)} />
+    </Input.Group>
+  );
+}
+
+export default PhoneField;

+ 90 - 0
src/components/DDComponents/TableField/index.js

@@ -0,0 +1,90 @@
+import React, { useMemo, useState, useEffect } from 'react';
+import { Button } from 'antd';
+import DDComponents from '../index';
+import style from './index.less';
+
+function TableField(props) {
+  const { item, onChange } = props;
+  const [value, setValue] = useState([]);
+
+  const getEmptyValue = () => {
+    let data = item.children.map(children => ({
+      name: children.props.id,
+      value: '',
+    }));
+    data.id = Math.random();
+    return data;
+  };
+  const getChildren = (item, data) => {
+    let cur = data.find(d => d.name == item.props.id);
+    const childrenChange = value => {
+      cur.value = value;
+      onChange(JSON.stringify(value));
+    };
+    return DDComponents({ item, onChange: childrenChange, value: cur.value });
+  };
+
+  // const localValue = useMemo(() => {
+  //   let v = [];
+  //   try {
+  //     v = JSON.parse(value);
+  //   } catch (error) {}
+  //   if (v.length == 0) {
+  //     let data = getEmptyValue();
+  //     v.push(data);
+  //   }
+  //   return v;
+  // }, [value]);
+
+  const addRow = () => {
+    let newValue = [...value, getEmptyValue()];
+    setValue(newValue);
+    onChange(JSON.stringify(newValue));
+  };
+
+  const removeRow = index => {
+    let newValue = [...value];
+    newValue.splice(index, 1);
+
+    setValue(newValue);
+    onChange(JSON.stringify(newValue));
+  };
+  useEffect(() => {
+    setValue([getEmptyValue()]);
+  }, []);
+
+  return (
+    <div>
+      <table className={style.table}>
+        <thead>
+          <tr>
+            <th></th>
+            {item.children.map(item => (
+              <th>{item.props.label}</th>
+            ))}
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          {value.map((curItem, index) => (
+            <tr key={curItem.id}>
+              <td>{index + 1}</td>
+              {item.children.map(item => (
+                <td>{getChildren(item, curItem)}</td>
+              ))}
+              <td>
+                {value.length > 1 && <a onClick={() => removeRow(index)}>删除</a>}
+                {/* <a>复制</a> */}
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+      <Button style={{ marginTop: 20 }} onClick={addRow}>
+        {item.props.actionName}
+      </Button>
+    </div>
+  );
+}
+
+export default TableField;

+ 16 - 0
src/components/DDComponents/TableField/index.less

@@ -0,0 +1,16 @@
+.table {
+  width: 100%;
+  td {
+    padding: 10px;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+  }
+  th {
+    padding: 10px;
+    color: #000000d9;
+    font-weight: 500;
+    text-align: left;
+    background: #fafafa;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+    transition: background 0.3s ease;
+  }
+}

+ 124 - 0
src/components/DDComponents/index.js

@@ -0,0 +1,124 @@
+import { Input, InputNumber, Select, DatePicker, Rate } from 'antd';
+import TableField from './TableField';
+import PhoneField from './PhoneField';
+import InnerContactField from './InnerContactField';
+import DepartmentField from './DepartmentField';
+import DDMultiSelectField from './DDMultiSelectField';
+import NumberField from './NumberField';
+import DDPhotoField from './DDPhotoField';
+import DDSelectField from './DDSelectField';
+import DDDateField from './DDDateField';
+import DDDateRangeField from './DDDateRangeField';
+import DDAttachment from './DDAttachment';
+
+const { Option } = Select;
+const { RangePicker } = DatePicker;
+
+export default function DDComponents(props) {
+  const { item, onChange } = props;
+  const {
+    id,
+    label,
+    bizAlias,
+    required,
+    placeholder,
+    options,
+    align,
+    statField,
+    hideLabel,
+    objOptions,
+    format,
+    pushToAttendance,
+    labelEditableFreeze,
+    requiredEditableFreeze,
+    unit,
+    extract,
+    link,
+    payEnable,
+    bizType,
+    childFieldVisible,
+    notPrint,
+    verticalPrint,
+    hiddenInApprovalDetail,
+    disabled,
+    notUpper,
+    children, // 子控件
+  } = item.props;
+  let component = null;
+  switch (item.componentName) {
+    case 'TextField': //单行输入
+      component = (
+        <Input
+          disabled={disabled}
+          placeholder={placeholder}
+          onChange={e => onChange?.(e.target.value)}
+        />
+      );
+      break;
+    case 'TextareaField': //多行输入
+      component = (
+        <Input.TextArea
+          disabled={disabled}
+          placeholder={placeholder}
+          onChange={e => onChange?.(e.target.value)}
+        />
+      );
+      break;
+    case 'NumberField': //数字输入
+      component = <NumberField disabled={disabled} unit={unit} onChange={onChange} />;
+      break;
+    case 'DDSelectField': //单选框
+      component = <DDSelectField options={options} onChange={onChange} disabled={disabled} />;
+      break;
+    case 'DDMultiSelectField': //多选框
+      component = <DDMultiSelectField disabled={disabled} options={options} onChange={onChange} />;
+      break;
+    case 'DDDateField': //日期控件
+      component = <DDDateField format={format} disabled={disabled} onChange={onChange} />;
+      break;
+    case 'DDDateRangeField': //时间区间控件
+      component = <DDDateRangeField format={format} disabled={disabled} onChange={onChange} />;
+      break;
+    case 'TextNote': //文本说明控件
+      console.info('文本说明控件!');
+      console.log(item);
+      break;
+    case 'PhoneField': //电话控件
+      component = <PhoneField onChange={onChange} />;
+      break;
+    case 'DDPhotoField': //图片控件
+      component = <DDPhotoField />;
+      break;
+    case 'MoneyField': //金额控件
+      component = <Input placeholder={placeholder} onChange={onChange} />;
+      break;
+    case 'TableField': //明细控件
+      component = <TableField item={item} />;
+      break;
+    case 'DDAttachment': //附件
+      // component = <DDAttachment />
+      // component = '附件控件未渲染!'
+      console.info('附件控件未渲染!');
+      break;
+    case 'InnerContactField': //联系人控件
+      component = <InnerContactField onChange={onChange}></InnerContactField>;
+      break;
+    case 'DepartmentField': //部门控件
+      component = <DepartmentField onChange={onChange} />;
+      break;
+    case 'RelateField': //关联审批单
+      component = '关联审批单控件未渲染!'
+      break;
+    case 'AddressField': //省市区控件
+      component = '省市区控件未渲染!'
+      break;
+    case 'StarRatingField': //评分控件
+      component = '评分控件未渲染!'
+      break;
+    case 'FormRelateField': //关联控件
+      component = '关联控件未渲染!'
+      break;
+  }
+
+  return component;
+}

+ 944 - 0
src/components/DataMeter/config.js

@@ -0,0 +1,944 @@
+export const CHILD_MAP = {
+  ProjectInfo: [
+    {
+      title: '项目编号',
+      key: 'Code',
+      show: true,
+    },
+    {
+      title: '项目工期',
+      key: 'Duration',
+      show: true,
+    },
+    {
+      title: '建设单位',
+      key: 'ConstructionUnit',
+      show: true,
+    },
+    {
+      title: '承建单位',
+      key: 'UndertakenUnit',
+      show: true,
+    },
+    {
+      title: '所在地',
+      key: 'Position',
+      show: true,
+    },
+
+    {
+      title: '项目规模',
+      key: 'Scale',
+      show: true,
+    },
+    {
+      title: '主体工艺',
+      key: 'MainProcess',
+      show: true,
+    },
+    {
+      title: '服务范围',
+      key: 'ServiceScope',
+      show: true,
+    },
+    {
+      title: '签约时间',
+      key: 'ContractTime',
+      show: true,
+    },
+    {
+      title: '运营服务时间',
+      key: 'ServiceTime',
+      show: true,
+    },
+    {
+      title: '合作模式',
+      key: 'CooperateMode',
+      show: true,
+    },
+    {
+      title: '出水执行标准',
+      key: 'WaterStandard',
+      show: true,
+    },
+    {
+      title: '核心管理人员',
+      key: 'Admin',
+      show: true,
+    },
+    {
+      title: '项目成员',
+      key: 'User',
+      show: true,
+    },
+  ],
+  AlarmCenter: [
+    { title: '全部', key: 4, show: true },
+    {
+      title: '待办事项',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '异常报警',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '逾期预警',
+      key: 2,
+      show: true,
+    },
+  ],
+  MessageCenter: [
+    {
+      title: '消息推送',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '新闻动态',
+      key: 2,
+      show: true,
+    },
+    {
+      title: '大事记',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '所获奖项',
+      key: 4,
+      show: true,
+    },
+  ],
+  PlanManagement: [
+    // {
+    //   title: '设计',
+    //   key: 1,
+    //   show: true,
+    // },
+    // {
+    //   title: '采购',
+    //   key: 2,
+    //   show: true,
+    // },
+    {
+      title: '到货',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '施工',
+      key: 2,
+      show: true,
+    },
+    {
+      title: '单机调试',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '系统调试',
+      key: 4,
+      show: true,
+    },
+    {
+      title: '试运行',
+      key: 5,
+      show: true,
+    },
+    // {
+    //   title: '维修',
+    //   key: 6,
+    //   show: true,
+    // },
+    {
+      title: '项目动态',
+      key: 7,
+      show: true,
+    },
+    // {
+    //   title: '巡检',
+    //   key: 7,
+    //   show: true,
+    // },
+    // {
+    //   title: '其它',
+    //   key: 8,
+    //   show: true,
+    // },
+  ],
+  Other: [
+    {
+      title: '安全操作规范',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '项目日志',
+      key: 2,
+      show: true,
+    },
+  ],
+  Monitor: [],
+  DebugData: [],
+  DataCenter: [
+    {
+      title: '水质管理',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '水质指标',
+      key: 2,
+      show: true,
+    },
+    {
+      title: '客户满意度',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '设备完好率',
+      key: 4,
+      show: true,
+    },
+    {
+      title: 'KPI',
+      key: 5,
+      show: true,
+      children: [
+        {
+          title: '累计EBITDA',
+          key: 11,
+          show: true,
+        },
+        {
+          title: '累计经营性净现金流',
+          key: 12,
+          show: true,
+        },
+      ],
+    },
+    {
+      title: '水量计费',
+      key: 6,
+      show: true,
+    },
+    {
+      title: '预算成本',
+      key: 7,
+      show: true,
+      children: [
+        {
+          title: '累计成本支出',
+          key: 13,
+          show: true,
+        },
+        {
+          title: '累计月度吨水成本',
+          key: 14,
+          show: true,
+        },
+      ],
+    },
+    {
+      title: '季度绩效',
+      key: 8,
+      show: true,
+    },
+    {
+      title: '化学实验室数据',
+      key: 9,
+      show: true,
+    },
+    {
+      title: '安全生产天',
+      key: 10,
+      show: true,
+    },
+    // {
+    //   title: '调试数据',
+    //   key: 11,
+    //   show: true,
+    // },
+  ],
+};
+
+export const DEFAULT_LAYOUT = [
+  {
+    i: '41',
+    key: 'AlarmCenter',
+    w: 8,
+    h: 5,
+    x: 28,
+    y: 14,
+    child: [
+      { title: '全部', key: 4, show: true },
+      { title: '待办事项', key: 3, show: true },
+      { title: '异常报警', key: 1, show: true },
+      { title: '逾期预警', key: 2, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 2,
+  },
+  {
+    i: '40',
+    key: 'DataCenter',
+    active: 11,
+    w: 6,
+    h: 5,
+    x: 42,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '39',
+    key: 'DataCenter',
+    active: '4',
+    w: 6,
+    h: 5,
+    x: 42,
+    y: 9,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '38',
+    key: 'DataCenter',
+    active: '2',
+    w: 6,
+    h: 5,
+    x: 42,
+    y: 4,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '37',
+    key: 'DataCenter',
+    active: 10,
+    w: 6,
+    h: 4,
+    x: 42,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '36',
+    key: 'DataCenter',
+    active: '1',
+    w: 6,
+    h: 5,
+    x: 36,
+    y: 4,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '35',
+    key: 'DataCenter',
+    active: 13,
+    w: 7,
+    h: 5,
+    x: 7,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '34',
+    key: 'DataCenter',
+    active: '8',
+    w: 7,
+    h: 5,
+    x: 0,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '33',
+    key: 'DataCenter',
+    active: 11,
+    w: 7,
+    h: 4,
+    x: 7,
+    y: 10,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '32',
+    key: 'DataCenter',
+    active: '2',
+    w: 7,
+    h: 5,
+    x: 7,
+    y: 5,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '31',
+    key: 'DataCenter',
+    active: '1',
+    w: 7,
+    h: 5,
+    x: 0,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '30',
+    key: 'DataCenter',
+    w: 7,
+    h: 5,
+    x: 7,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 12,
+  },
+  {
+    i: '29',
+    key: 'DataCenter',
+    w: 6,
+    h: 5,
+    x: 36,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '20',
+    key: 'DataCenter',
+    w: 6,
+    h: 5,
+    x: 36,
+    y: 9,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 3,
+  },
+  {
+    i: '14',
+    key: 'DataCenter',
+    w: 7,
+    h: 5,
+    x: 0,
+    y: 5,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 3,
+  },
+  {
+    i: '12',
+    key: 'PlanManagement',
+    w: 6,
+    h: 4,
+    x: 36,
+    y: 0,
+    child: [
+      { title: '设计', key: 1, show: true },
+      { title: '采购', key: 2, show: true },
+      { title: '施工', key: 3, show: true },
+      { title: '调试', key: 4, show: true },
+      { title: '保养', key: 5, show: true },
+      { title: '维修', key: 6, show: true },
+      { title: '项目动态', key: 7, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 1,
+  },
+  {
+    i: '10',
+    key: 'PlanManagement',
+    w: 7,
+    h: 4,
+    x: 0,
+    y: 10,
+    child: [
+      { title: '设计', key: 1, show: true },
+      { title: '采购', key: 2, show: true },
+      { title: '施工', key: 3, show: true },
+      { title: '调试', key: 4, show: true },
+      { title: '保养', key: 5, show: true },
+      { title: '维修', key: 6, show: true },
+      { title: '项目动态', key: 7, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 2,
+  },
+  {
+    i: '8',
+    key: 'ProjectInfo',
+    w: 14,
+    h: 5,
+    x: 14,
+    y: 14,
+    active: 1,
+    child: [
+      { title: '项目编号', key: 'Code', show: true },
+      { title: '项目工期', key: 'Duration', show: true },
+      { title: '建设单位', key: 'ConstructionUnit', show: true },
+      { title: '承建单位', key: 'UndertakenUnit', show: true },
+      { title: '所在地', key: 'Position', show: true },
+      { title: '项目规模', key: 'Scale', show: true },
+      { title: '主体工艺', key: 'MainProcess', show: true },
+      { title: '服务范围', key: 'ServiceScope', show: true },
+      { title: '签约时间', key: 'ContractTime', show: true },
+      { title: '运营服务时间', key: 'ServiceTime', show: true },
+      { title: '合作模式', key: 'CooperateMode', show: true },
+      { title: '出水执行标准', key: 'WaterStandard', show: true },
+      { title: '核心管理人员', key: 'Admin', show: true },
+      { title: '项目成员', key: 'User', show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  { i: '7', key: 'Map', w: 22, h: 14, x: 14, y: 0, moved: false, static: false },
+];
+export const DEFAULT_MAP_LAYOUT = [
+  {
+    i: '20',
+    key: 'DataCenter',
+    w: 14,
+    h: 7,
+    x: 0,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '19',
+    key: 'DataCenter',
+    w: 7,
+    h: 7,
+    x: 7,
+    y: 13,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 9,
+  },
+  {
+    i: '18',
+    key: 'DataCenter',
+    w: 7,
+    h: 6,
+    x: 0,
+    y: 13,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 3,
+  },
+  {
+    i: '17',
+    key: 'DataCenter',
+    w: 7,
+    h: 6,
+    x: 7,
+    y: 7,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 7,
+  },
+  {
+    i: '15',
+    key: 'DataCenter',
+    w: 13,
+    h: 6,
+    x: 35,
+    y: 6,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 6,
+  },
+  {
+    i: '12',
+    key: 'DataCenter',
+    w: 7,
+    h: 6,
+    x: 0,
+    y: 7,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 5,
+  },
+  {
+    i: '11',
+    key: 'DataCenter',
+    w: 13,
+    h: 7,
+    x: 35,
+    y: 12,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 4,
+  },
+  { i: '7', key: 'Map', w: 21, h: 12, x: 14, y: 0, moved: false, static: false },
+  {
+    i: '5',
+    key: 'AlarmCenter',
+    w: 10,
+    h: 6,
+    x: 25,
+    y: 12,
+    active: 3,
+    child: [
+      { title: '待办事项', key: 3, show: true },
+      { title: '异常报警', key: 1, show: true },
+      { title: '项目预警', key: 2, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '3',
+    key: 'DataCenter',
+    w: 13,
+    h: 6,
+    x: 35,
+    y: 0,
+    active: 10,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '2',
+    key: 'MessageCenter',
+    w: 11,
+    h: 6,
+    x: 14,
+    y: 12,
+    active: 1,
+    child: [
+      { title: '消息推送', key: 1, show: true },
+      { title: '新闻动态', key: 2, show: true },
+      { title: '大事记', key: 3, show: true },
+      { title: '所获奖项', key: 4, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+];
+
+export const U3D_PATH_STATE = {
+  ProjectInfo: 1,
+  AlarmCenter: {
+    3: 53,
+    1: 39,
+    2: 54,
+  },
+  MessageCenter: {
+    1: 54,
+  },
+  FileManagement: [27, 44],
+  Monitor: 100,
+  PlanManagement: {
+    1: 48,
+    2: 49,
+    3: 50,
+    4: 50,
+    5: 51,
+  },
+  Other: {
+    2: 35
+  },
+};
+
+export const COL_COLS = 48;
+export const ROW_COLS = 24;

+ 233 - 0
src/components/Flow/components/judgeComponent/index.tsx

@@ -0,0 +1,233 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Checkbox, Select, TreeSelect } from 'antd';
+import { DeleteOutlined } from '@ant-design/icons';
+import { ComponentName, FormItem } from '../../node/judgeNode/mapServe';
+import { InputNumberFiled } from '../../node/fields';
+import { connect } from 'dva';
+
+const { Option } = Select;
+
+interface Condition {
+  smallValue?: number;
+  smallSign?: number;
+  bigValue?: number;
+  bigSign?: number;
+}
+
+export interface JudgeType {
+  type: string;
+  id: string;
+  condition?: Condition;
+  values?: any[];
+}
+
+export const JudgeOptions = [
+  { label: '小于', value: 1 },
+  { label: '大于', value: 2 },
+  { label: '小于等于', value: 3 },
+  { label: '等于', value: 4 },
+  { label: '大于等于', value: 5 },
+  { label: '介于(两个数之间)', value: 6 },
+];
+export const SiginOptions = [
+  { label: '<', value: 1 },
+  { label: '≤', value: 3 },
+];
+
+const RenderJudge = (props: any) => {
+  const { formItems = '', onChange, depUserTree } = props;
+  let formData: FormItem[] = formItems ? JSON.parse(formItems) : [];
+
+  const handleChange = (values: any[], item: FormItem, condition?: Condition) => {
+    const itemCur = formData.find(cur => cur.props.id == item.props.id);
+    let judge: JudgeType = {
+      id: item.props.id,
+      values:
+        values.length == 0 && itemCur && itemCur.judge?.values ? itemCur.judge.values : values,
+      type: item.componentName,
+      condition:
+        !condition && itemCur && itemCur.judge?.condition ? itemCur.judge.condition : condition,
+    };
+    const newItem: FormItem = {
+      ...item,
+      judge,
+    };
+    const index = formData.findIndex(cur => cur.props.id == item.props.id);
+    if (index != -1) {
+      formData[index] = newItem;
+    } else {
+      formData.push(newItem);
+    }
+    onChange(formData);
+  };
+
+  const getTreeNode = (pid, dep) => {
+    return {
+      id: dep.ID,
+      pId: pid,
+      value: dep.ID,
+      title: dep.Name,
+      isLeaf: false,
+    };
+  };
+  const getResult = (pid, items) => {
+    let result = [];
+    items.forEach(item => {
+      let node = getTreeNode(pid, item);
+      result.push(node);
+      if (item.children) {
+        let node = getResult(item.ID, item.children);
+        result = [...result, ...node];
+      }
+    });
+    return result;
+  };
+
+  const handleDelete = (item: FormItem) => {
+    const index = formData.findIndex(cur => cur.props.id == item.props.id);
+    if (index != -1) {
+      formData.splice(index, 1);
+      onChange(formData);
+    }
+  };
+
+  const handleTreeChange = (values: (string | number)[], item: FormItem) => {
+    const newValues = values.map(cur => {
+      if (typeof cur == 'string' && cur.includes('||')) {
+        return { type: 'user', value: Number(cur.split('||')[0]), origin: cur };
+      } else {
+        return { type: 'dep', value: cur, origin: cur };
+      }
+    });
+    handleChange(newValues, item);
+  };
+
+  const RenderComp = (item: FormItem) => {
+    let component;
+    switch (item.componentName) {
+      case ComponentName.Inner:
+        component = (
+          <>
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <div>发起人</div>
+              <DeleteOutlined onClick={() => handleDelete(item)} />
+            </div>
+            <TreeSelect
+              showSearch
+              multiple
+              allowClear
+              defaultValue={item.judge?.values?.map(item => item.origin)}
+              style={{ width: '100%' }}
+              placeholder="请选择部门"
+              treeData={depUserTree}
+              onChange={values => {
+                handleTreeChange(values, item);
+                // handleChange(values, item)
+              }}
+            />
+          </>
+        );
+        break;
+      case ComponentName.Depart:
+        break;
+      case ComponentName.Money:
+      case ComponentName.Number:
+        const { judge, props } = item;
+        component = (
+          <>
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <div>{props.label}</div>
+              <DeleteOutlined onClick={() => handleDelete(item)} />
+            </div>
+            <Select
+              options={JudgeOptions}
+              defaultValue={judge?.values[0]}
+              onChange={(value: number) => {
+                let newCondition: Condition = null;
+                if (judge && judge.values && judge.values.length > 0) {
+                  if (judge?.values[0] == 6 && value != 6) newCondition = {};
+                  else if (judge.values[0] != 6 && value == 6) newCondition = {};
+                }
+                handleChange([value], item, newCondition);
+              }}
+            />
+            {judge?.values[0] == 6 ? (
+              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                <InputNumberFiled
+                  value={judge?.condition?.smallValue}
+                  onChange={value => {
+                    handleChange([], item, { ...judge?.condition, smallValue: value });
+                  }}
+                />
+                <Select
+                  defaultValue={judge?.condition?.smallSign}
+                  onChange={(value: number) => {
+                    handleChange([], item, { ...judge?.condition, smallSign: Number(value) });
+                  }}
+                >
+                  {SiginOptions.map(item => (
+                    <Option key={item.value}>{item.label}</Option>
+                  ))}
+                </Select>
+                <span>N</span>
+                <Select
+                  defaultValue={judge?.condition?.bigSign}
+                  onChange={(value: number) => {
+                    handleChange([], item, { ...judge?.condition, bigSign: Number(value) });
+                  }}
+                >
+                  {SiginOptions.map(item => (
+                    <Option key={item.value}>{item.label}</Option>
+                  ))}
+                </Select>
+                <InputNumberFiled
+                  value={judge?.condition?.bigValue}
+                  onChange={value => {
+                    handleChange([], item, { ...judge?.condition, bigValue: value });
+                  }}
+                />
+              </div>
+            ) : (
+              <InputNumberFiled
+                value={judge?.condition?.smallValue}
+                onChange={value => {
+                  handleChange([], item, { smallValue: value });
+                }}
+              />
+            )}
+          </>
+        );
+        break;
+
+      case ComponentName.MultiSelect:
+      case ComponentName.Select:
+        const options = item.props.options;
+        component = (
+          <>
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <div>{item.props.label}</div>
+              <DeleteOutlined onClick={() => handleDelete(item)} />
+            </div>
+            <Checkbox.Group
+              defaultValue={item.judge?.values}
+              onChange={(values: any) => handleChange(values, item)}
+            >
+              {options.map((cur: any, idx: number) => {
+                return (
+                  <Checkbox key={`${cur}_${idx}`} value={cur}>
+                    {cur}
+                  </Checkbox>
+                );
+              })}
+            </Checkbox.Group>
+          </>
+        );
+        break;
+    }
+    return component;
+  };
+
+  return <>{formData.map(item => RenderComp(item))}</>;
+};
+
+export default connect(({ user }) => ({ depUserTree: user.depUserTree }))(RenderJudge);

+ 76 - 0
src/components/Flow/components/judgeModal/index.tsx

@@ -0,0 +1,76 @@
+import { Button, Checkbox, Modal } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+import React, { useMemo, useState } from 'react';
+import { ComponentName, FormItem } from '../../node/judgeNode/mapServe';
+
+const AddCondition = (props: any) => {
+  const { items = [], formItems = '', onOk } = props;
+  const [visible, setVisible] = useState(false);
+  const [values, setValues] = useState([]);
+
+  let formData = useMemo(() => {
+    let formDat = formItems ? JSON.parse(formItems) : [];
+    setValues(formDat.map(cur => cur.props.id));
+    return formDat;
+  }, [formItems]);
+
+  const data: FormItem[] = items
+    .filter((item: FormItem) => {
+      return (
+        (item.componentName == ComponentName.Inner ||
+          // item.componentName == 'DepartmentField' ||
+          item.componentName == ComponentName.Select ||
+          item.componentName == ComponentName.MultiSelect ||
+          item.componentName == ComponentName.Number ||
+          item.componentName == ComponentName.Money) &&
+        item.props.required
+      );
+    })
+    .map(item => {
+      return {
+        label: item.props.label,
+        ...item,
+      };
+    });
+
+  const onChange = (values: []) => {
+    setValues(values);
+  };
+
+  const handleOk = () => {
+    let resultData: FormItem[] = [];
+    values.map(value => {
+      const item = formData.find(cur => cur.props.id == value);
+      if (item) {
+        resultData.push(item);
+      } else {
+        const item = items.find(cur => cur.props.id == value);
+        resultData.push(item);
+      }
+    });
+    onOk(resultData);
+    setVisible(false);
+  };
+
+  return (
+    <>
+      <div className={`group`}>
+        <Button icon={<PlusOutlined />} onClick={() => setVisible(true)}>
+          选择条件
+        </Button>
+        还有{data.length - values.length}组可用条件
+      </div>
+      <Modal title="选择条件" visible={visible} onOk={handleOk} onCancel={() => setVisible(false)}>
+        <p>请选择用来区分审批流程的条件字段 ,已选{values.length}个</p>
+        <Checkbox.Group value={values} onChange={onChange}>
+          {data.map((item: FormItem) => (
+            <Checkbox key={item.props.id} value={item.props.id}>
+              {item.props.label}
+            </Checkbox>
+          ))}
+        </Checkbox.Group>
+      </Modal>
+    </>
+  );
+};
+export default AddCondition;

+ 2 - 1
src/components/Flow/config-toolbar.ts

@@ -231,10 +231,11 @@ namespace NSToolbarConfig {
                   id: item.id,
                   source: item.source,
                   target: item.target,
-                  attrs: item.attrs
+                  attr: JSON.stringify(item.attrs)
                 };
               });
               console.log(data);
+              console.log(JSON.stringify(data));
               localStorage.graphData = JSON.stringify(data);
               return null;
             },

+ 109 - 5
src/components/Flow/index.tsx

@@ -1,5 +1,5 @@
-import { IAppLoad } from '@antv/xflow';
-import React, { useRef, useEffect } from 'react';
+import { IAppLoad, NsGraphCmd } from '@antv/xflow';
+import React, { useRef, useEffect, useImperativeHandle } from 'react';
 /** 交互组件 */
 import {
   /** XFlow核心组件 */
@@ -33,7 +33,7 @@ import { useCmdConfig, initGraphCmds } from './config-cmd';
 /** 配置Menu */
 import { useMenuConfig } from './config-menu';
 /** 配置Toolbar */
-import { useToolbarConfig } from './config-toolbar';
+import { TOOLBAR_ITEMS, useToolbarConfig } from './config-toolbar';
 /** 配置快捷键 */
 import { useKeybindingConfig } from './config-keybinding';
 import { registerNode } from './node/registerNode';
@@ -44,15 +44,17 @@ import CustomFlowchartFormPanel from './node/FlowFormPanel';
 
 import '@antv/xflow/dist/index.css';
 import './index.less';
+import { TYPE } from './node/auditNode/mapServe';
 
 export interface IProps {
   meta: { flowId: string; type: 'edit' };
   flowDetail: any;
   onSelectNode?: Function;
+  parentRef?: any;
 }
 
 export const Demo: React.FC<IProps> = props => {
-  const { meta, flowDetail } = props;
+  const { meta, flowDetail, parentRef } = props;
   const isEdit = meta.type == 'edit';
   const toolbarConfig = useToolbarConfig();
   const menuConfig = useMenuConfig();
@@ -61,6 +63,91 @@ export const Demo: React.FC<IProps> = props => {
   const appRef = useRef<IApplication>();
   const commandConfig: any = useCmdConfig(props);
 
+  // 封装供外部主动调用的接口
+  useImperativeHandle(parentRef, () => ({
+    getGraphData: async cb => {
+      appRef.current.commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
+        TOOLBAR_ITEMS.SAVE_GRAPH_DATA,
+        {
+          saveGraphDataService: (meta, graphData) => {
+            let data = JSON.parse(JSON.stringify(graphData));
+            let simpleNodes = data?.nodes?.map(curNode => {
+              let childrenNodes = data.edges
+                .map(edge => {
+                  if (edge.source?.cell == curNode.id) {
+                    return data.nodes.find(item => item.id == edge.target?.cell);
+                  }
+                })
+                .filter(item => item);
+              //按优先级排序子节点
+              const children = childrenNodes
+                .sort((a, b) => a.priority - b.priority)
+                .map(item => item.id);
+              const node = {
+                id: curNode.id,
+                type: curNode.type,
+                //条件节点
+                formItems:
+                  curNode.type == TYPE.JUDGE && !curNode.formItems ? '[]' : curNode.formItems,
+                priority: curNode.priority,
+                //审批节点
+                initiator: curNode.type == TYPE.INITIATOR ? curNode.initiator : null,
+                audits: curNode.type == TYPE.AUDIT ? curNode.audits : null,
+                children: children,
+              };
+              return node;
+            });
+            data.nodes = data.nodes.map(item => {
+              let newItem = JSON.parse(JSON.stringify(item));
+              delete newItem.incomingEdges;
+              delete newItem.originData;
+              delete newItem.outgoingEdges;
+              delete newItem.ports.groups;
+              // let ports = { ...item.ports };
+              // delete ports.groups;
+              // return {
+              //   id: item.id,
+              //   renderKey: item.renderKey,
+              //   name: item.name,
+              //   label: item.label,
+              //   width: item.width,
+              //   height: item.height,
+              //   ports: ports,
+              //   isCustom: item.isCustom,
+              //   parentKey: item.parentKey,
+              //   x: item.x,
+              //   y: item.y,
+              //   zIndex: item.zIndex,
+              //   count: item.count,
+              // };
+              return newItem;
+            });
+            // graphData.edges = []
+            data.edges = data.edges.map(item => {
+              // delete item.data;
+              // delete item.attrs;
+              // delete item.sourcePort;
+              // delete item.sourcePortId;
+              // delete item.targetPort;
+              // delete item.targetPortId;
+              // delete item.zIndex;
+              return {
+                id: item.id,
+                source: item.source,
+                target: item.target,
+                // attr: JSON.stringify(item.attrs),
+              };
+            });
+
+            console.log(simpleNodes);
+            console.log(JSON.stringify(data));
+            cb?.(JSON.stringify(data), JSON.stringify(simpleNodes));
+            return data;
+          },
+        }
+      );
+    },
+  }));
   /**
    * @param app 当前XFlow工作空间
    * @param extensionRegistry 当前XFlow配置项
@@ -68,6 +155,7 @@ export const Demo: React.FC<IProps> = props => {
   const onLoad: IAppLoad = async app => {
     appRef.current = app;
     graphRef.current = await app.getGraphInstance();
+    // graphRef.current.disableSnapline()
     renderGraph();
   };
 
@@ -88,11 +176,19 @@ export const Demo: React.FC<IProps> = props => {
   };
   const getConfig = () => {
     const defaultOption = {
+      grid: 1,
       mousewheel: {
         enabled: true,
         /** 将鼠标位置作为中心缩放 */
         zoomAtMousePosition: true,
       },
+      resizing: {
+        enabled: true,
+        minWidth: 0,
+        minHeight: 0,
+        preserveAspectRatio: false,
+      },
+      snapline: false,
     };
     return isEdit
       ? defaultOption
@@ -185,4 +281,12 @@ export const Demo: React.FC<IProps> = props => {
   );
 };
 
-export default Demo;
+// 高阶组件
+const DemoHoc = Demo => {
+  const forwardRef = (props, ref) => {
+    return <Demo parentRef={ref} {...props}></Demo>;
+  };
+  return React.forwardRef(forwardRef);
+};
+
+export default DemoHoc(Demo);

+ 132 - 0
src/components/Flow/node/auditNode/index.tsx

@@ -0,0 +1,132 @@
+import React, { useMemo } from 'react';
+AuditServe;
+import AuditServe, { IDTYPE, TYPE } from './mapServe';
+import { connect } from 'dva';
+export { AuditServe };
+
+const CustomRect = props => {
+  const { size = { width: 130, height: 50 }, data, depUserTree, roleList, userList } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize, type = 2, initiator, audits } = data;
+
+  const contentText = useMemo(() => {
+    let text = '请选择审批人';
+    switch (type) {
+      case TYPE.AUDIT:
+        text = '请选择审批人';
+        break;
+      case TYPE.INITIATOR:
+        text = '所有人';
+        break;
+      case TYPE.COPYMAN:
+        text = '请选择抄送人';
+        break;
+    }
+    const getName = (id, data) => {
+      let name = '';
+      for (let i = 0; i < data.length; i++) {
+        let item = data[i];
+        if (item.ID == id) {
+          return item.title;
+        } else if (item.children?.length > 0) {
+          let title = getName(id, item.children);
+          if (title) return title;
+        }
+      }
+      return name;
+    };
+    if (type != TYPE.AUDIT) {
+      if (initiator?.length > 0) {
+        const list = initiator
+          .map(item => {
+            return getName(item.value, depUserTree);
+          })
+          .filter(item => item);
+        return list.join(',');
+      } else {
+        return text;
+      }
+    } else {
+      if (audits?.length > 0) {
+        return audits[0].type == IDTYPE.ROLE
+          ? roleList.find(item => item.ID == audits[0].value)?.Name
+          : userList.find(item => item.ID == audits[0].value)?.CName;
+        // return roleList.find(item => item.ID == audits[0].value)?.Name;
+      } else {
+        return text;
+      }
+    }
+  }, [initiator, type, audits, depUserTree, roleList]);
+
+  // const type: TYPE = 0;
+  const titleDom = () => {
+    let color = COLOR.AUDIT;
+    let text = label == '动作节点' ? TITLETEXT.AUDIT : label;
+    switch (type) {
+      case TYPE.AUDIT:
+        color = COLOR.AUDIT;
+        text = TITLETEXT.AUDIT;
+        break;
+      case TYPE.INITIATOR:
+        color = COLOR.INITIATOR;
+        text = TITLETEXT.INITIATOR;
+        break;
+      case TYPE.COPYMAN:
+        color = COLOR.COPYMAN;
+        text = TITLETEXT.COPYMAN;
+        break;
+    }
+    return (
+      <div
+        style={{
+          width: '100%',
+          height: `${fontSize + 16}px`,
+          paddingLeft: '6px',
+          backgroundColor: color,
+          color: 'white',
+          fontSize,
+        }}
+      >
+        {text}
+      </div>
+    );
+  };
+  return (
+    <div
+      style={{
+        width,
+        height,
+        boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)',
+      }}
+    >
+      {titleDom()}
+      <div
+        style={{
+          paddingLeft: '6px',
+          height: `${height - fontSize - 16}px`,
+          lineHeight: `${height - fontSize - 16}px`,
+          fontSize,
+        }}
+      >
+        {contentText}
+      </div>
+    </div>
+  );
+};
+
+export default connect(({ user }) => ({
+  depUserTree: user.depUserTree,
+  roleList: user.roleList,
+  userList: user.list,
+}))(CustomRect);
+
+const enum COLOR {
+  INITIATOR = '#576A95',
+  AUDIT = '#FF943E',
+  COPYMAN = '#3296FA',
+}
+const enum TITLETEXT {
+  INITIATOR = '发起人',
+  AUDIT = '审批人',
+  COPYMAN = '抄送人',
+}

+ 283 - 0
src/components/Flow/node/auditNode/mapServe.tsx

@@ -0,0 +1,283 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { FlowchartFormWrapper } from '@antv/xflow';
+import { Position, Size, ColorPicker, InputNumberFiled, InputFiled, SelectField } from '../fields';
+import { PREFIX } from '../constants';
+import { connect } from 'dva';
+import { UnityAction } from '@/utils/utils';
+import { Button, Radio, RadioChangeEvent, Select, TreeSelect } from 'antd';
+
+const { Option } = Select;
+
+export const enum TYPE {
+  INITIATOR = 1,
+  AUDIT,
+  JUDGE,
+  COPYMAN,
+}
+
+export const typeOption = [
+  { label: '审批人', value: TYPE.AUDIT },
+  { label: '发起人', value: TYPE.INITIATOR },
+  { label: '抄送人', value: TYPE.COPYMAN },
+];
+
+export interface IConfig {
+  label?: string;
+  x?: number;
+  y?: number;
+  width?: number;
+  height?: number;
+  count?: number;
+  fontSize?: number;
+  fontFill?: string;
+  fill?: string;
+  stroke?: string;
+  flow_id?: string;
+  flow_node_id?: string;
+  process_code?: string;
+  type: TYPE;
+  initiator: { type: string; value: number; origin?: string | number; name?: string }[];
+  //审批人目前只支持单选角色或者单选人
+  audits: { type: string; value: number; origin?: string | number; name?: string }[];
+}
+
+export const enum IDTYPE {
+  DEP = 'dep',
+  USER = 'user',
+  ROLE = 'role',
+}
+
+const Component = (props: any) => {
+  const defaultConfig = {
+    type: TYPE.AUDIT,
+  };
+  const { config, plugin = {}, depUserTree, roleList, userList } = props;
+  const { updateNode } = plugin;
+  const [isRole, setIsRole] = useState(1); ///按角色还是个人
+
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...defaultConfig,
+    ...config,
+  });
+  const onNodeConfigChange = (key: string, value: number | string | object) => {
+    if (key) {
+      setNodeConfig({
+        ...nodeConfig,
+        [key]: value,
+      });
+      updateNode({
+        type: nodeConfig.type ? nodeConfig.type : TYPE.AUDIT,
+        [key]: value,
+      });
+    } else if (value instanceof Object) {
+      setNodeConfig({
+        ...nodeConfig,
+        ...value,
+      });
+      updateNode({
+        type: nodeConfig.type ? nodeConfig.type : TYPE.AUDIT,
+        ...value,
+      });
+    }
+  };
+
+  useEffect(() => {
+    const bool = nodeConfig.audits?.some(item => item.type == IDTYPE.USER) ? 0 : 1;
+    console.log(bool);
+    setIsRole(bool);
+  }, []);
+  console.log('===================', nodeConfig);
+
+  const handleTreeChange = (values: (string | number)[]) => {
+    const newValues = values.map(cur => {
+      if (typeof cur == 'string' && cur.includes('||')) {
+        return { type: IDTYPE.USER, value: Number(cur.split('||')[0]), origin: cur };
+      } else {
+        return { type: IDTYPE.DEP, value: cur, origin: cur };
+      }
+    });
+    onNodeConfigChange('initiator', newValues);
+  };
+
+  const onSave = () => {
+    UnityAction.emit('NODE_SAVE', nodeConfig);
+  };
+  useEffect(() => {
+    setNodeConfig({
+      ...defaultConfig,
+      ...config,
+    });
+  }, [config]);
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={nodeConfig.label}
+          onChange={value => {
+            onNodeConfigChange('label', value);
+          }}
+        />
+        <SelectField
+          label="节点类型"
+          value={nodeConfig.type}
+          onChange={value => {
+            onNodeConfigChange('type', value);
+          }}
+          options={typeOption}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>样式</h5>
+        <Position
+          x={nodeConfig.x}
+          y={nodeConfig.y}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <Size
+          width={nodeConfig.width}
+          height={nodeConfig.height}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <ColorPicker
+          label="填充"
+          value={nodeConfig.fill}
+          onChange={(value: string) => {
+            onNodeConfigChange('fill', value);
+          }}
+        />
+        <ColorPicker
+          label="边框"
+          value={nodeConfig.stroke}
+          onChange={(value: string) => {
+            onNodeConfigChange('stroke', value);
+          }}
+        />
+        <InputNumberFiled
+          label="消息数量"
+          value={nodeConfig.count}
+          onChange={value => {
+            onNodeConfigChange('count', value);
+          }}
+        />
+        <div className={`${PREFIX}-node-text-style`}>
+          <InputNumberFiled
+            label="字号"
+            value={nodeConfig.fontSize}
+            width={68}
+            onChange={value => {
+              onNodeConfigChange('fontSize', value);
+            }}
+          />
+          <ColorPicker
+            value={nodeConfig.fontFill}
+            onChange={(value: string) => {
+              onNodeConfigChange('fontFill', value);
+            }}
+          />
+        </div>
+        {nodeConfig.type != TYPE.AUDIT && (
+          <div className="group">
+            <label>{typeOption.find(item => item.value == nodeConfig.type)?.label || '-'}</label>
+            <TreeSelect
+              showSearch
+              multiple
+              allowClear
+              value={nodeConfig.initiator?.map(item => item.origin)}
+              style={{ width: '80%' }}
+              placeholder="请选择"
+              treeData={depUserTree}
+              onChange={values => {
+                handleTreeChange(values);
+              }}
+            />
+          </div>
+        )}
+
+        {nodeConfig.type == TYPE.AUDIT && (
+          <div className={`${PREFIX}-panel-group`}>
+            <h5>审批人</h5>
+            <Radio.Group
+              onChange={(e: RadioChangeEvent) => {
+                console.log(e.target.value);
+                setIsRole(e.target.value);
+                onNodeConfigChange('audits', []);
+              }}
+              value={isRole}
+            >
+              <Radio value={1}>发起人自选</Radio>
+              <Radio value={0}>指定人</Radio>
+            </Radio.Group>
+            <div className="group">
+              {isRole ? (
+                <>
+                  <label>审批角色</label>
+                  <Select
+                    showSearch
+                    style={{ width: '100%' }}
+                    value={nodeConfig.audits?.map(item => item.value)}
+                    filterOption={(input, option) => option.props.children.indexOf(input) >= 0}
+                    onChange={value => {
+                      onNodeConfigChange('audits', [{ type: IDTYPE.ROLE, value: Number(value) }]);
+                    }}
+                  >
+                    {roleList &&
+                      roleList.map(item => (
+                        <Option key={item.ID} value={item.ID}>
+                          {item.Name}
+                        </Option>
+                      ))}
+                  </Select>
+                </>
+              ) : (
+                <>
+                  <label>审批人</label>
+                  <Select
+                    showSearch
+                    style={{ width: '100%' }}
+                    value={nodeConfig.audits?.map(item => item.value)}
+                    onChange={value => {
+                      onNodeConfigChange('audits', [{ type: IDTYPE.USER, value: Number(value) }]);
+                    }}
+                    filterOption={(input, option) => option.props.children.indexOf(input) >= 0}
+                  >
+                    {(userList || []).map(item => (
+                      <Option key={item.ID} value={item.ID}>
+                        {item.CName}
+                      </Option>
+                    ))}
+                  </Select>
+                </>
+              )}
+            </div>
+          </div>
+        )}
+      </div>
+
+      <Button style={{ marginTop: 20 }} type="primary" onClick={onSave}>
+        保存
+      </Button>
+    </div>
+  );
+};
+
+function RecthServe(props: any) {
+  return (
+    <FlowchartFormWrapper {...props}>
+      {(config, plugin) => <Component {...props} plugin={plugin} config={config} />}
+    </FlowchartFormWrapper>
+  );
+}
+
+export default connect(({ xflow, flow, user }) => ({
+  auditList: xflow.auditList,
+  depUserTree: user.depUserTree,
+  roleList: user.roleList,
+  userList: user.list,
+}))(RecthServe);

+ 3 - 1
src/components/Flow/node/circle/index.tsx

@@ -33,9 +33,11 @@ export default function CustomRect(props) {
           display: 'flex',
           justifyContent: 'center',
           alignItems: 'center',
+          padding: "0px 8px",
+          lineHeight: 1.2
         }}
       >
-        <span>{label}</span>
+        <span style={{ textAlign: 'center', lineHeight: '20px' }}>{label}</span>
       </div>
     </Badge>
   );

+ 2 - 0
src/components/Flow/node/circle/mapServe.tsx

@@ -19,6 +19,7 @@ export interface IConfig {
   stroke?: string;
   flow_id?: string;
   flow_node_id?: string;
+  process_code?: string;
 }
 
 const Component = (props: any) => {
@@ -79,6 +80,7 @@ const Component = (props: any) => {
             onNodeConfigChange(null, {
               flow_node_id: audit.list.FlowNodes[0]?.id,
               flow_id: value,
+              process_code: audit.list.process_code,
             });
           }}
           options={auditList.map(item => {

+ 15 - 33
src/components/Flow/node/control-map-service/components/edge.tsx

@@ -1,17 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import { FlowchartFormWrapper } from '@antv/xflow';
-import {
-  ColorPicker,
-  InputNumberFiled,
-  InputFiled,
-  SelectField,
-} from '../../fields';
-import {
-  PREFIX,
-  DefaultEdgeConfig,
-  ArrowStrokeMaps,
-  ArrowMaps,
-} from '../../constants';
+import { ColorPicker, InputNumberFiled, InputFiled, SelectField } from '../../fields';
+import { PREFIX, DefaultEdgeConfig, ArrowStrokeMaps, ArrowMaps } from '../../constants';
 
 export type MarkerCfg = {
   width?: number;
@@ -43,7 +33,7 @@ const EdgeComponent = (props: any) => {
     ...DefaultEdgeConfig,
     ...config,
   });
-  console.log(DefaultEdgeConfig, config);
+  console.log(edgeConfig);
 
   useEffect(() => {
     setEdgeConfig({
@@ -75,15 +65,13 @@ const EdgeComponent = (props: any) => {
   const getSrokeDashValue = () => {
     const { attrs = {} } = edgeConfig;
     const { line = {} } = attrs;
-    return line.strokeDasharray && line.strokeDasharray[0] == 5
-      ? 'dash'
-      : 'solid';
+    return line.strokeDasharray && line.strokeDasharray[0] == 5 ? 'dash' : 'solid';
   };
 
   const onEdgeConfigChange = (
     key: string,
     value: number | string | object,
-    type: string = 'line',
+    type: string = 'line'
   ) => {
     /** 全量更新,简化逻辑 */
     if (key === 'arrow') {
@@ -116,7 +104,7 @@ const EdgeComponent = (props: any) => {
         [key]: value,
       },
       type,
-      key === 'arrow' ? 'arrow' : '',
+      key === 'arrow' ? 'arrow' : ''
     );
   };
 
@@ -127,7 +115,7 @@ const EdgeComponent = (props: any) => {
         <InputFiled
           label="标签"
           value={edgeConfig.label}
-          onChange={(value) => {
+          onChange={value => {
             onEdgeConfigChange('label', value);
           }}
         />
@@ -157,12 +145,8 @@ const EdgeComponent = (props: any) => {
               value: 'none',
             },
           ]}
-          onChange={(value) => {
-            onEdgeConfigChange(
-              'arrow',
-              ArrowMaps[value as keyof typeof ArrowMaps],
-              'line',
-            );
+          onChange={value => {
+            onEdgeConfigChange('arrow', ArrowMaps[value as keyof typeof ArrowMaps], 'line');
           }}
         />
 
@@ -181,18 +165,18 @@ const EdgeComponent = (props: any) => {
                 value: 'dash',
               },
             ]}
-            onChange={(value) => {
+            onChange={value => {
               onEdgeConfigChange(
                 'strokeDasharray',
                 ArrowStrokeMaps[value as keyof typeof ArrowStrokeMaps],
-                'line',
+                'line'
               );
             }}
           />
           <InputNumberFiled
             value={getAttrs('strokeWidth')}
             min={1}
-            onChange={(value) => {
+            onChange={value => {
               onEdgeConfigChange('strokeWidth', value, 'line');
             }}
           />
@@ -213,7 +197,7 @@ const EdgeComponent = (props: any) => {
             min={10}
             width={68}
             value={getAttrs('fontSize', 'text') || 12}
-            onChange={(value) => {
+            onChange={value => {
               onEdgeConfigChange('fontSize', value, 'text');
             }}
           />
@@ -228,12 +212,10 @@ const EdgeComponent = (props: any) => {
     </div>
   );
 };
-export const EdgeService: React.FC<any> = (props) => {
+export const EdgeService: React.FC<any> = props => {
   return (
     <FlowchartFormWrapper {...props} type="edge">
-      {(config, plugin) => (
-        <EdgeComponent {...props} plugin={plugin} config={config} />
-      )}
+      {(config, plugin) => <EdgeComponent {...props} plugin={plugin} config={config} />}
     </FlowchartFormWrapper>
   );
 };

+ 1 - 1
src/components/Flow/node/fields/radio.tsx

@@ -29,7 +29,7 @@ const SelectField: React.FC<IProps> = props => {
         }}
       >
         {options.map(item => (
-          <Radio value={item.value}>{item.label}</Radio>
+          <Radio key={`${item.value}-${item.label}`} value={item.value}>{item.label}</Radio>
         ))}
       </Radio.Group>
     </div>

+ 113 - 0
src/components/Flow/node/judgeNode/index.tsx

@@ -0,0 +1,113 @@
+import React, { useEffect, useMemo } from 'react';
+judgeServe;
+import judgeServe, { ComponentName, FormItem } from './mapServe';
+import { JudgeType, JudgeOptions, SiginOptions } from '../../components/judgeComponent';
+import { connect } from 'dva';
+export { judgeServe };
+
+const JudgeRect = props => {
+  const { size = { width: 130, height: 50 }, data, depUserTree } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize, type, priority, formItems } = data;
+
+  const contentText = useMemo(() => {
+    let text = [];
+    const getName = (id, data) => {
+      let name = '';
+      for (let i = 0; i < data.length; i++) {
+        let item = data[i];
+        if (item.ID == id) {
+          return item.title;
+        } else if (item.children?.length > 0) {
+          let title = getName(id, item.children);
+          if (title) return title;
+        }
+      }
+      return name;
+    };
+
+    if (formItems) {
+      let data: FormItem[] = JSON.parse(formItems);
+      data.forEach((item: FormItem) => {
+        let judge: JudgeType = item.judge;
+        const label: String = item.props.label;
+        switch (judge?.type) {
+          case ComponentName.Inner:
+            const list = judge?.values
+              .map(item => {
+                return getName(item.value, depUserTree);
+              })
+              .filter(item => item);
+            text.push('发起人属于:' + list.join('或'));
+            break;
+          case ComponentName.Number:
+            const type: Number = judge.values[0];
+            const condition = judge.condition;
+            if (!condition) break;
+            if (type != 6) {
+              let JudgeLabel = JudgeOptions.find(item => item.value == type)?.label;
+
+              text.push(`${label} ${JudgeLabel} ${condition.smallValue}`);
+            } else {
+              const { smallSign, smallValue, bigSign, bigValue } = condition;
+              if (!smallSign || !smallValue || !bigSign || !bigValue) break;
+              const getSigin = (sigin: Number) =>
+                SiginOptions.find(item => item.value == sigin)?.label;
+
+              text.push(
+                `${smallValue} ${getSigin(smallSign)} ${label} ${getSigin(bigSign)} ${bigValue} `
+              );
+            }
+            break;
+          case ComponentName.Select:
+          case ComponentName.MultiSelect:
+            const values = judge.values;
+            text.push(`${label} ${values.join(' 且 ')}`);
+            break;
+        }
+      });
+    }
+    if (text.length <= 0) text.push('其他条件进入此流程');
+    return text;
+  }, [formItems, depUserTree]);
+
+  return (
+    <div
+      style={{
+        width,
+        height,
+        padding: '6px',
+        boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)',
+      }}
+    >
+      <span style={{ color: '#7E8185', float: 'right', fontSize: '8px' }}>优先级{priority}</span>
+      <div style={{ color: '#15BC83' }}>{label}</div>
+      <div
+        style={{
+          height: `${height - 32}px`,
+          overflow: 'hidden',
+          textOverflow: 'ellipsis',
+          fontSize,
+        }}
+      >
+        {(contentText || []).map((text, index) => (
+          <>
+            {index != 0 && (
+              <>
+                <br />
+                并且
+                <br />
+              </>
+            )}
+
+            {text}
+          </>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export default connect(({ user }) => ({
+  depUserTree: user.depUserTree,
+}))(JudgeRect);

+ 268 - 0
src/components/Flow/node/judgeNode/mapServe.tsx

@@ -0,0 +1,268 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { FlowchartFormWrapper } from '@antv/xflow';
+import { Position, Size, ColorPicker, InputNumberFiled, InputFiled, SelectField } from '../fields';
+import { PREFIX } from '../constants';
+import { connect } from 'dva';
+import { UnityAction } from '@/utils/utils';
+import { Button } from 'antd';
+import AddCondition from '../../components/judgeModal';
+import RenderJudge, { JudgeType } from '../../components/judgeComponent';
+import { TYPE } from '../auditNode/mapServe';
+
+// export const enum TYPE {
+//   AUDIT,
+//   INITIATOR,
+//   COPYMAN,
+// }
+
+export const enum ComponentName {
+  Inner = 'InnerContactField',
+  Depart = 'DepartmentField',
+  Select = 'DDSelectField',
+  Money = 'MoneyField',
+  MultiSelect = 'DDMultiSelectField',
+  Number = 'NumberField',
+}
+
+export interface IConfig {
+  label?: string;
+  x?: number;
+  y?: number;
+  width?: number;
+  height?: number;
+  count?: number;
+  fontSize?: number;
+  fontFill?: string;
+  fill?: string;
+  stroke?: string;
+  flow_id?: string;
+  flow_node_id?: string;
+  process_code?: string;
+  type: TYPE;
+  formItems: string;
+  priority?: number; //优先级
+}
+
+export interface FormItem {
+  componentName: string;
+  props: {
+    label: string;
+    id: string;
+    options?: string[];
+    required: boolean;
+    placeholder?: string;
+  };
+  judge?: JudgeType;
+}
+
+const Component = (props: any) => {
+  const { config, plugin = {}, formItems } = props;
+  const { updateNode } = plugin;
+
+  // const formData: FormItem[] = [
+  //   {
+  //     componentName: 'InnerContactField',
+  //     props: {
+  //       label: '申请人',
+  //       id: 'InnerContactField-JEQRQ5VH',
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'DepartmentField',
+  //     props: {
+  //       label: '选择部门',
+  //       id: 'DepartmentField_1VALX0GTRNVK0',
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'DDSelectField',
+  //     props: {
+  //       id: 'DDSelectField_LWFCJRK508W0',
+  //       label: '文件类型',
+  //       options: [
+  //         '{"value":"新增|FT008","key":"option_1IXTB1VDV9EO0"}',
+  //         '{"value":"补充|FT009","key":"option_1DVWNV9BNZNK0"}',
+  //       ],
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'DDSelectField',
+  //     props: {
+  //       id: 'DDSelectField_21WUQ0OWR7R40',
+  //       label: '是否外带',
+  //       options: [
+  //         '{"value":"是","key":"option_DUSP6XANFKO0"}',
+  //         '{"value":"否","key":"option_CC5VQ63ZLJK0"}',
+  //       ],
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'MoneyField',
+  //     props: {
+  //       id: 'MoneyField-JSCUC2GG',
+  //       label: '文件金额(元)',
+  //       placeholder: '请输入金额',
+  //       required: true,
+  //     },
+  //   },
+  // ];
+
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...config,
+  });
+  const onNodeConfigChange = (key: string, value: number | string | object) => {
+    if (key) {
+      setNodeConfig({
+        ...nodeConfig,
+        [key]: value,
+      });
+      updateNode({
+        type: TYPE.JUDGE,
+        [key]: value,
+      });
+    } else if (value instanceof Object) {
+      setNodeConfig({
+        ...nodeConfig,
+        ...value,
+      });
+      updateNode({
+        type: TYPE.JUDGE,
+        ...value,
+      });
+    }
+  };
+
+  const onSave = () => {
+    UnityAction.emit('NODE_SAVE', nodeConfig);
+  };
+
+  useEffect(() => {
+    setNodeConfig({
+      ...config,
+    });
+  }, [config]);
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={nodeConfig.label}
+          onChange={value => {
+            onNodeConfigChange('label', value);
+          }}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>样式</h5>
+        <Position
+          x={nodeConfig.x}
+          y={nodeConfig.y}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <Size
+          width={nodeConfig.width}
+          height={nodeConfig.height}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <ColorPicker
+          label="填充"
+          value={nodeConfig.fill}
+          onChange={(value: string) => {
+            onNodeConfigChange('fill', value);
+          }}
+        />
+        <ColorPicker
+          label="边框"
+          value={nodeConfig.stroke}
+          onChange={(value: string) => {
+            onNodeConfigChange('stroke', value);
+          }}
+        />
+        <InputNumberFiled
+          label="消息数量"
+          value={nodeConfig.count}
+          onChange={value => {
+            onNodeConfigChange('count', value);
+          }}
+        />
+        <div className={`${PREFIX}-node-text-style`}>
+          <InputNumberFiled
+            label="字号"
+            value={nodeConfig.fontSize}
+            width={68}
+            onChange={value => {
+              onNodeConfigChange('fontSize', value);
+            }}
+          />
+          <ColorPicker
+            value={nodeConfig.fontFill}
+            onChange={(value: string) => {
+              onNodeConfigChange('fontFill', value);
+            }}
+          />
+        </div>
+        <InputNumberFiled
+          label="优先级"
+          value={nodeConfig.priority}
+          width={68}
+          onChange={value => {
+            onNodeConfigChange('priority', value);
+          }}
+        />
+        <AddCondition
+          items={formItems}
+          formItems={nodeConfig.formItems}
+          onOk={(values: FormItem[]) => {
+            console.log('===formItems===', values);
+            let newFormItems = [];
+            let oldFormItems = JSON.parse(nodeConfig.formItems || '[]');
+            values.forEach(item => {
+              let id = item.props.id;
+              // 判断是否含有旧的item
+              let oldItem = oldFormItems.find(formItem => formItem.props.id == id);
+              if (oldItem) {
+                newFormItems.push(oldItem);
+              } else {
+                newFormItems.push(item);
+              }
+            });
+            onNodeConfigChange('formItems', JSON.stringify(newFormItems));
+          }}
+        />
+        <RenderJudge
+          formItems={nodeConfig.formItems}
+          onChange={(values: FormItem[]) => {
+            console.log('===formItems===', values);
+            onNodeConfigChange('formItems', JSON.stringify(values));
+          }}
+        />
+      </div>
+
+      <Button style={{ marginTop: 20 }} type="primary" onClick={onSave}>
+        保存
+      </Button>
+    </div>
+  );
+};
+
+function RecthServe(props: any) {
+  return (
+    <FlowchartFormWrapper {...props}>
+      {(config, plugin) => <Component {...props} plugin={plugin} config={config} />}
+    </FlowchartFormWrapper>
+  );
+}
+
+export default connect(({ xflow }) => ({ auditList: xflow.auditList, formItems: xflow.formData }))(
+  RecthServe
+);

+ 67 - 10
src/components/Flow/node/rect/mapServe.tsx

@@ -1,6 +1,6 @@
 import React, { useState, useEffect } from 'react';
 import { FlowchartFormWrapper } from '@antv/xflow';
-import { Button, message } from 'antd';
+import { Button, message, Select } from 'antd';
 import LuckyExcel from 'luckyexcel';
 import {
   Position,
@@ -13,6 +13,12 @@ import {
 } from '../fields';
 import { PREFIX } from '../constants';
 import { UnityAction } from '@/utils/utils';
+import { connect } from 'dva';
+
+interface ExcelInfo {
+  file_name?: string;
+  excel_cols?: any;
+}
 
 export interface IConfig {
   label?: string;
@@ -30,17 +36,21 @@ export interface IConfig {
   bom_template?: string;
   version_name?: string;
   data?: any;
-  excel_info?: any;
+  excel_info?: ExcelInfo;
+  role_list?: string;
 }
 
 const defaultConfig: IConfig = {
   muti_version: 1,
   is_start_node: 0,
+  excel_info: { file_name: '' },
 };
 
 const Component = (props: any) => {
-  const { config, plugin = {} } = props;
+  const { config, plugin = {}, roleList } = props;
   const { updateNode } = plugin;
+  const [options, setOptions] = useState([]);
+  const [fileName, setFileName] = useState('');
   const [nodeConfig, setNodeConfig] = useState<IConfig>({
     ...defaultConfig,
     ...config,
@@ -79,13 +89,13 @@ const Component = (props: any) => {
       const sheet = exportJson.sheets[0];
       let titleCell = [];
       sheet.celldata.forEach(item => {
-        if(item.r == 0) {
+        if (item.r == 0) {
           // 标题头
-          titleCell.push(item)
+          titleCell.push(item);
         }
         // 生成cid
-        item.v.cid = `${item.r}-${item.c}`
-      })
+        item.v.cid = `${item.r}-${item.c}`;
+      });
       let cell = titleCell.map(item => {
         let value = '';
         if (item.v?.v) {
@@ -111,8 +121,36 @@ const Component = (props: any) => {
       ...config,
     });
   }, [config]);
+
+  const updataFileName = (name: string) => {
+    var idx = name?.lastIndexOf('/');
+    let str = name.substring(idx + 1, name.length);
+    setFileName(str);
+  };
+
+  useEffect(() => {
+    if (config.bom_template) updataFileName(config.bom_template);
+    if (nodeConfig.excel_info?.file_name) updataFileName(nodeConfig.excel_info.file_name);
+  }, [nodeConfig.bom_template, nodeConfig.excel_info.file_name]);
   // console.log(nodeConfig, config)
 
+  useEffect(() => {
+    if (!roleList || roleList.length <= 0) return;
+    let op = [];
+    console.log(roleList);
+    roleList
+      .filter(cur => cur.RoleType == 4)
+      .forEach(item => {
+        op.push({ label: `${item.Name}(${item.ID})`, value: item.ID });
+      });
+    setOptions(op);
+    console.log(op);
+  }, [roleList]);
+
+  // const handleFileNameClick = () => {
+  //   if (nodeConfig.bom_template) window.open(nodeConfig.bom_template);
+  // };
+
   return (
     <div className={`${PREFIX}-panel-body`}>
       <div className={`${PREFIX}-panel-group`}>
@@ -152,7 +190,7 @@ const Component = (props: any) => {
         {nodeConfig.is_start_node == 1 && (
           <>
             <InputFiled
-              label="版本名称"
+              label="模板名称"
               value={nodeConfig.version_name}
               onChange={value => {
                 onNodeConfigChange('version_name', value);
@@ -163,9 +201,26 @@ const Component = (props: any) => {
               onChange={url => onNodeConfigChange('bom_template', url)}
               beforeUpload={beforeUpload}
             />
-            <div>{nodeConfig.excel_info?.file_name}</div>
+            {/* <div onClick={handleFileNameClick}>{fileName}</div> */}
+            <a href={nodeConfig.bom_template}>{fileName}</a>
           </>
         )}
+        <div className="group">
+          <label>权限</label>
+          <Select
+            value={
+              nodeConfig.role_list ? nodeConfig.role_list.split(',').map(item => Number(item)) : []
+            }
+            mode="multiple"
+            allowClear
+            style={{ width: '100%' }}
+            placeholder="选择权限"
+            onChange={(v: number[]) => {
+              onNodeConfigChange('role_list', v.join(','));
+            }}
+            options={options}
+          />
+        </div>
       </div>
       <div className={`${PREFIX}-panel-group`}>
         <h5>样式</h5>
@@ -229,10 +284,12 @@ const Component = (props: any) => {
   );
 };
 
-export default function RecthServe(props: any) {
+function RecthServe(props: any) {
   return (
     <FlowchartFormWrapper {...props}>
       {(config, plugin) => <Component {...props} plugin={plugin} config={config} />}
     </FlowchartFormWrapper>
   );
 }
+
+export default connect(({ user }) => ({ roleList: user.roleList }))(RecthServe);

+ 21 - 3
src/components/Flow/node/registerNode.tsx

@@ -1,10 +1,11 @@
-import React from "react"
+import React from 'react';
 import Rect, { RectServe } from './rect';
 import Circle, { CircleServe } from './circle';
+import AuditNode, { AuditServe } from './auditNode';
 
+import judgeNode, { judgeServe } from './judgeNode';
 
 export const registerNode = [
-
   {
     component: Rect,
     popover: () => <div>业务组件</div>,
@@ -23,5 +24,22 @@ export const registerNode = [
     label: '审批节点',
     service: CircleServe,
   },
-
+  {
+    component: AuditNode,
+    popover: () => <div>动作节点</div>,
+    name: 'custom-audit',
+    width: 130,
+    height: 50,
+    label: '动作节点',
+    service: AuditServe,
+  },
+  {
+    component: judgeNode,
+    popover: () => <div>条件节点</div>,
+    name: 'custom-judge',
+    width: 130,
+    height: 50,
+    label: '条件节点',
+    service: judgeServe,
+  },
 ];

+ 43 - 43
src/global.less

@@ -42,20 +42,20 @@ ol {
 }
 
 @media (max-width: @screen-xs) {
-  .ant-table {
-    width: 100%;
-    overflow-x: auto;
-    &-thead > tr,
-    &-tbody > tr {
-      > th,
-      > td {
-        white-space: pre;
-        > span {
-          display: block;
-        }
-      }
-    }
-  }
+  // .ant-table {
+  //   width: 100%;
+  //   overflow-x: auto;
+  //   &-thead > tr,
+  //   &-tbody > tr {
+  //     > th,
+  //     > td {
+  //       white-space: pre;
+  //       > span {
+  //         display: block;
+  //       }
+  //     }
+  //   }
+  // }
 }
 
 //.ant-table-wrapper {
@@ -109,35 +109,35 @@ ol {
   }
 }
 
-.ant-table {
-  table {
-    border: 1px solid #e8e8e8;
-    border-right: 0;
-    border-bottom: 0;
-
-    .ant-table-bordered .ant-table-thead > tr > th, .ant-table-bordered .ant-table-tbody > tr > td {
-      border-right: 1px solid #e8e8e8;
-    }
-    .ant-table-thead > tr > th {
-      color: rgba(0, 0, 0, 0.85);
-      font-weight: 500;
-      text-align: left;
-      background: #fafafa;
-      border-bottom: 1px solid #e8e8e8;
-      transition: background 0.3s ease;
-      border-right: 1px solid #e8e8e8;
-    }
-    .ant-table-tbody > tr > td {
-      border-bottom: 1px solid #e8e8e8;
-      border-right: 1px solid #e8e8e8;
-      background: white;
-    }
-
-    .ant-table-thead > tr:first-child > th:first-child {
-      border-top-left-radius: 4px;
-    }
-  }
-}
+// .ant-table {
+//   table {
+//     border: 1px solid #e8e8e8;
+//     border-right: 0;
+//     border-bottom: 0;
+
+//     .ant-table-bordered .ant-table-thead > tr > th, .ant-table-bordered .ant-table-tbody > tr > td {
+//       border-right: 1px solid #e8e8e8;
+//     }
+//     .ant-table-thead > tr > th {
+//       color: rgba(0, 0, 0, 0.85);
+//       font-weight: 500;
+//       text-align: left;
+//       background: #fafafa;
+//       border-bottom: 1px solid #e8e8e8;
+//       transition: background 0.3s ease;
+//       border-right: 1px solid #e8e8e8;
+//     }
+//     .ant-table-tbody > tr > td {
+//       border-bottom: 1px solid #e8e8e8;
+//       border-right: 1px solid #e8e8e8;
+//       background: white;
+//     }
+
+//     .ant-table-thead > tr:first-child > th:first-child {
+//       border-top-left-radius: 4px;
+//     }
+//   }
+// }
 
 /*滚动条整体样式*/
 ::-webkit-scrollbar {

+ 624 - 0
src/models/dataMeterNew.js

@@ -0,0 +1,624 @@
+import {
+  getProject,
+  getFaultAnalysis,
+  getIssueList,
+  getProjectActive,
+  getNotificationList,
+  getMediaList,
+  getMonitorList,
+  getUserGuide,
+  getDailyList,
+  getProjectProgress,
+  deleteFile,
+  getProjectTotalProgress,
+  getLayoutOptions,
+  saveLayoutOptions,
+  getProjectAlarm,
+  getBreakdownRecord,
+  getProjectList,
+  getAutoPatrol,
+  getBreakdownList,
+  getPatrolRecord,
+} from '@/services/DataMeter';
+import { queryProjectFileList } from '@/services/FileAdmin';
+import { getprojectPlanProgress, getProjectRealProgress } from '@/services/projectPlanProgress';
+import { getRepairRecord } from '@/services/EquipmentMaintenance';
+import { queryConfigList, queryTemplateList } from '@/services/CharUtils';
+import { queryPatrolRecord } from '@/services/patrol';
+import { message } from 'antd';
+import { DEFAULT_LAYOUT, DEFAULT_MAP_LAYOUT } from '@/components/DataMeter/config';
+import { getDefaultConfList } from '@/services/ProjectAdmin';
+const PLAN_TYPE = {
+  15: 1,
+  3: 3,
+  12: 4,
+};
+
+export default {
+  namespace: 'dataMeterNew',
+  state: {
+    projectDetail: {},
+    faultAnalysis: [],
+    issueList: [],
+    projectActive: [],
+    news: [],
+    memorabilia: [],
+    awards: [],
+    notificationList: [],
+    projectAlarmList: [],
+    repairList: [],
+    breakdownList: [],
+    mediaImgList: [],
+    mediaVideo: null,
+    monitorList: [],
+    userGuideList: [],
+    dailyList: [],
+    projectProgress: null,
+    projectTotalProgress: {
+      AllProgress: 0,
+    },
+    layout: {
+      0: {
+        config_json: [],
+      },
+      1: {
+        config_json: [],
+      },
+      2: {
+        config_json: [],
+      },
+    },
+    chartConfigList: [],
+    realProgress: {},
+    progress: {},
+    // currentChart: {}
+    projectList: [],
+    autoReport: {},
+    patrolRecordDetail: {},
+    patrolList: [],
+    fileList: [],
+    defaultConf: {},
+    layoutAdmin: {
+      0: {
+        config_json: [],
+      },
+      1: {
+        config_json: [],
+      },
+      2: {
+        config_json: [],
+      },
+    },
+  },
+
+  effects: {
+    *getAutoPatrol({ payload, callback }, { call, put }) {
+      const response = yield call(getAutoPatrol, payload);
+      if (response) {
+        // 查询数据报表详情
+        const { data } = yield call(queryPatrolRecord, { recordId: response.data[0].Id });
+        if (data) {
+          const creatorName = data.CreatorUser && data.CreatorUser.CName;
+          var status = {};
+          let Items = [];
+          const getItems = (item, patrolType) => {
+            let key;
+            if (patrolType == 1) {
+              key = item.PatrolName;
+            } else {
+              key = item.DeviceCode + '-' + item.DeviceName;
+            }
+            item.patrolType = patrolType;
+            item.ThresholdEnum = item.TemplateItem.ThresholdEnum;
+            item.Type = item.TemplateItem.Type;
+            if (!status[key]) status[key] = { normal: 0, error: 0 };
+            if (item.Status == 1) {
+              status[key].error++;
+            } else {
+              status[key].normal++;
+            }
+            return item;
+          };
+          data.ItemsExtend.forEach(item => {
+            console.log(Items);
+            if (item.PatrolCardRecordItemAssocThreshold) {
+              var arr = item.PatrolCardRecordItemAssocThreshold.map(i => getItems(i, 1));
+              Items = Items.concat(arr);
+            } else {
+              Items.push(getItems(item, 0));
+            }
+          });
+          data.Items = Items;
+
+          data.Points.forEach(item => {
+            let key;
+            if (item.PatrolType == 1) {
+              key = item.DeviceName;
+            } else {
+              key = item.DeviceCode + '-' + item.DeviceName;
+            }
+            item.creatorName = creatorName;
+            item.level = `${item.ExceptionLevel || '-'}/${item.DeviceLevel || '-'}`;
+            item.status = status[key] || { normal: 0, error: 0 };
+          });
+          console.log(data);
+          callback && callback(data);
+          yield put({
+            type: 'save',
+            payload: { autoReport: data },
+          });
+        }
+      }
+    },
+    *getProject({ payload }, { call, put }) {
+      const response = yield call(getProject, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectDetail: response.data },
+        });
+      }
+    },
+    *getFaultAnalysis({ payload }, { call, put }) {
+      const response = yield call(getFaultAnalysis, payload);
+      if (response) {
+        var list = [];
+        response.data.forEach(item => {
+          item.Details.forEach(err => {
+            list.push({
+              ...item,
+              ...err,
+            });
+          });
+        });
+        yield put({
+          type: 'save',
+          payload: { faultAnalysis: list },
+        });
+      }
+    },
+    *getIssueList({ payload }, { call, put }) {
+      const response = yield call(getIssueList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { issueList: response.data.list },
+        });
+      }
+    },
+    *getRepairRecord({ payload }, { call, put }) {
+      const response = yield call(getRepairRecord, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { repairList: response.data.list },
+        });
+      }
+    },
+    *getBreakdownRecord({ payload }, { call, put }) {
+      const response = yield call(getBreakdownList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { breakdownList: response.data.list },
+        });
+      }
+    },
+    *getPatrolRecord({ payload }, { call, put }) {
+      const response = yield call(getPatrolRecord, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { patrolList: response.data.list },
+        });
+      }
+    },
+    *getProjectAlarm({ payload }, { call, put }) {
+      const response = yield call(getProjectAlarm, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectAlarmList: response.data.list },
+        });
+      }
+    },
+    *getProjectActive({ payload }, { call, put }) {
+      const response = yield call(getProjectActive, payload);
+      if (response) {
+        let stateName;
+
+        // 0-项目动态 1-新闻动态 2-大事记 3-所获奖项
+        switch (payload.type) {
+          case 0:
+            stateName = 'projectActive';
+            break;
+          case 1:
+            stateName = 'news';
+            break;
+          case 2:
+            stateName = 'memorabilia';
+            break;
+          case 3:
+            stateName = 'awards';
+            break;
+        }
+        if (!stateName) return;
+        yield put({
+          type: 'save',
+          payload: { [stateName]: response.data.list },
+        });
+      }
+    },
+    *getNotificationList({ payload }, { call, put }) {
+      const response = yield call(getNotificationList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { notificationList: response.data.list },
+        });
+      }
+    },
+    *getMediaList({ payload }, { call, put }) {
+      const response = yield call(getMediaList, payload);
+      if (response) {
+        var mediaVideo = null,
+          mediaImgList = [];
+        response.data.forEach(item => {
+          if (/\.mp4$/.test(item.Name)) {
+            if (!mediaVideo) {
+              mediaVideo = item;
+            }
+          } else {
+            mediaImgList.push(item);
+          }
+        });
+        yield put({
+          type: 'save',
+          payload: {
+            mediaVideo,
+            mediaImgList,
+          },
+        });
+      }
+    },
+    *getMonitorList({ payload }, { call, put }) {
+      const response = yield call(getMonitorList, payload);
+      if (response) {
+        var obj = JSON.parse(response.data.config_json) || {};
+        yield put({
+          type: 'save',
+          // payload: { monitorList: response.data.list },
+          payload: { monitorList: Object.keys(obj).map(key => obj[key]) || [] },
+        });
+      }
+    },
+    *getUserGuide({ payload }, { call, put }) {
+      const response = yield call(getUserGuide, payload);
+      if (response) {
+        var list = [];
+        response.data.list.forEach(item => {
+          item.UserGuideFiles.forEach(file => {
+            list.push({
+              ...item,
+              ...file,
+            });
+          });
+        });
+        yield put({
+          type: 'save',
+          payload: { userGuideList: list },
+        });
+      }
+    },
+    *getDailyList({ payload }, { call, put }) {
+      const response = yield call(getDailyList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { dailyList: response.data.list },
+        });
+      }
+    },
+
+    *getProjectPlanProgress({ payload, callback }, { call, put }) {
+      const response = yield call(getprojectPlanProgress, payload);
+      if (response) {
+        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] = '';
+          }
+        });
+        yield put({
+          type: 'save',
+          payload: { progress },
+        });
+      }
+    },
+    *getProjectRealProgress({ payload }, { call, put, select }) {
+      const calcProgress = data => {
+        let count = 0,
+          total = 0;
+        data.forEach(item => {
+          total += item.TotalCount;
+          count += item.RealCount;
+        });
+        return ((count / total) * 100).toFixed(2);
+      };
+      let { realProgress } = yield select(s => s.dataMeterNew);
+      console.log(realProgress);
+      var keys = Object.keys(PLAN_TYPE);
+      for (let i = 0; i < keys.length; i++) {
+        let k = keys[i];
+        var response = yield call(getProjectRealProgress, {
+          ...payload,
+          type: PLAN_TYPE[k],
+        });
+        if (response) {
+          let tempRealProgress = response.data.Task.map(item => ({
+            ...item,
+            RealCount: item.FinishList.length || 0,
+            TotalCount: item.DeviceCodes ? item.DeviceCodes.length : 0,
+            children: null,
+          }));
+          console.log(tempRealProgress);
+          realProgress[k] = calcProgress(tempRealProgress);
+        }
+      }
+      yield put({
+        type: 'save',
+        payload: { realProgress: { ...realProgress } },
+      });
+    },
+    *getProjectProgress({ payload }, { call, put }) {
+      const response = yield call(getProjectProgress, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectProgress: response.data },
+        });
+      }
+    },
+    *deleteFile({ payload }, { call, put }) {
+      const response = yield call(deleteFile, payload);
+      if (response) {
+        yield put({
+          type: 'getMediaList',
+          payload: payload,
+        });
+      }
+    },
+    *getProjectTotalProgress({ payload }, { call, put }) {
+      const response = yield call(getProjectTotalProgress, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectTotalProgress: response.data },
+        });
+      }
+    },
+    *getLayoutOptions({ payload }, { call, put, select }) {
+      // if (payload.is_default == 1) {
+      //   // 默认配置的projectId为0
+      //   payload.projectId = 0;
+      // }
+      const response = yield call(getLayoutOptions, payload);
+      if (response) {
+        var layout = yield select(state => state.dataMeterNew.layout);
+        if (payload.is_default == 1) {
+          layout = yield select(state => state.dataMeterNew.layoutAdmin);
+        }
+        var defaultLayout = payload.sub_module === 0 ? DEFAULT_MAP_LAYOUT : DEFAULT_LAYOUT;
+        var data = response.data;
+        try {
+          data.config_json = JSON.parse(data.config_json);
+          // response.data.config_json = defaultLayout;
+        } catch (error) {
+          // 解析失败则使用默认布局
+          data.config_json = defaultLayout;
+          console.log(error);
+        }
+        layout[payload.sub_module] = data;
+        if (payload.is_default == 0)
+          yield put({
+            type: 'save',
+            payload: { layout: { ...layout } },
+          });
+        else
+          yield put({
+            type: 'save',
+            payload: { layoutAdmin: { ...layout } },
+          });
+      }
+    },
+    *saveLayoutOptions({ payload }, { call, put, select }) {
+      // if (payload.is_default == 1) {
+      //   // 默认配置的projectId为0
+      //   payload.projectId = 0;
+      // }
+      if (payload.config_json == '[]') {
+        message.error('保存的配置不能为空');
+        return;
+      }
+      var response = yield call(saveLayoutOptions, payload);
+      if (response) {
+        var layout = yield select(state => state.dataMeterNew.layout);
+        if (payload.is_default == 1) {
+          layout = yield select(state => state.dataMeterNew.layoutAdmin);
+        }
+        console.log(payload.config_json);
+        layout[payload.sub_module].config_json = JSON.parse(payload.config_json);
+        if (payload.is_default == 0)
+          yield put({
+            type: 'save',
+            payload: { layout: { ...layout } },
+          });
+        else
+          yield put({
+            type: 'save',
+            payload: { layoutAdmin: { ...layout } },
+          });
+      }
+    },
+    *resizeLayout({ payload }, { call, select, put }) {
+      // 默认配置的projectId为0
+      const response = yield call(getLayoutOptions, {
+        ...payload,
+        // projectId: 0,
+      });
+      if (response) {
+        var layout = yield select(state => state.dataMeterNew.layout);
+        var defaultLayout = payload.sub_module === 0 ? DEFAULT_MAP_LAYOUT : DEFAULT_LAYOUT;
+        var config_json = response.data.config_json
+          ? JSON.parse(response.data.config_json)
+          : defaultLayout;
+        // response.data.config_json = defaultLayout;
+
+        // 将默认配置保存到本地
+        layout[payload.sub_module].config_json = config_json;
+        yield put({
+          type: 'save',
+          payload: { layout: { ...layout } },
+        });
+        yield call(saveLayoutOptions, {
+          ...payload,
+          config_json: JSON.stringify(config_json),
+          is_default: 0,
+        });
+      }
+    },
+    *resizeDefaultLayout({ payload }, { call, select, put }) {
+      var layout = yield select(state => state.dataMeterNew.layout);
+      var config_json = payload.sub_module === 0 ? DEFAULT_MAP_LAYOUT : DEFAULT_LAYOUT;
+
+      // 将默认配置保存到本地
+      layout[payload.sub_module].config_json = config_json;
+      yield put({
+        type: 'save',
+        payload: { layout: { ...layout } },
+      });
+      yield call(saveLayoutOptions, {
+        ...payload,
+        config_json: JSON.stringify(config_json),
+        is_default: 0,
+      });
+    },
+    *queryConfigList({ payload, callback }, { call, put }) {
+      const {
+        data: { list: tplList },
+      } = yield call(queryTemplateList, { pageSize: 999 });
+      const {
+        data: { list: configList },
+      } = yield call(queryConfigList, {
+        ...payload,
+        isNew: 1,
+        pageSize: 9999,
+      });
+      let list = [];
+      configList.map(item => {
+        let template = tplList.find(tpl => tpl.ID == item.type);
+        item.options = JSON.parse(item.options);
+        item.template = template;
+        list.push(item);
+      });
+      callback && callback(list);
+      yield put({
+        type: 'save',
+        payload: {
+          chartConfigList: list,
+        },
+      });
+    },
+    *queryConfig({ payload, callback }, { call, put }) {
+      const response = yield call(queryConfigList, payload);
+      if (response) {
+        var currentChart = response.data.list[0];
+        currentChart.options = JSON.parse(currentChart.options);
+        let { options, configs, type } = currentChart;
+
+        // 将接口返回的实时数据更新到图表中
+        if (type != 'pfd') {
+          if (!options.series) {
+            callback && callback(currentChart);
+          }
+          let series = options.series[0];
+          if (type == 'pie') {
+            configs.forEach((item, index) => {
+              series.data[index].value = item.value;
+            });
+          } else if (series.type == 'bar') {
+            configs.forEach((item, index) => {
+              series.data[index] = item.value;
+            });
+          }
+        }
+        callback && callback(currentChart);
+        // yield put({
+        //   type: 'save',
+        //   payload: {
+        //     currentChart,
+        //   },
+        // });
+      }
+    },
+    *getProjectList({ payload, callback }, { call, put }) {
+      const response = yield call(getProjectList, payload);
+      if (response) {
+        let nowDate = new Date();
+        (response.data.list || []).map(item => {
+          let type;
+          if (!item.EndDate) {
+            type = 2;
+          } else {
+            type = nowDate >= new Date(item.EndDate) ? 2 : 1;
+          }
+          item.type = type;
+        });
+        yield put({
+          type: 'save',
+          payload: { projectList: response.data.list },
+        });
+        console.log(response.data.list);
+        callback && callback(response.data.list);
+      }
+    },
+    *queryProjectFileList({ payload }, { call, put, select }) {
+      try {
+        const response = yield call(queryProjectFileList, {
+          ...payload,
+          deviceCode: -1,
+        });
+        if (response) {
+          yield put({
+            type: 'save',
+            payload: { fileList: response.data },
+          });
+        }
+      } catch (e) {
+        console.error(e);
+      }
+    },
+    *getDefaultConfList({ payload }, { call, put }) {
+      const response = yield call(getDefaultConfList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { defaultConf: response.data },
+        });
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 80 - 5
src/models/user.js

@@ -2,14 +2,56 @@ import {
   query as queryUsers,
   queryCurrent,
   queryCurrentV2,
+  queryUserRole,
   queryUnreadNotification,
   SetNotificationRead,
 } from '@/services/user';
+import { queryDepV2 } from '@/services/approval';
 import { ShowUnreadNotification } from '@/utils/utils';
 import { queryDeviceItemRealtimeData } from '@/services/DeviceAdmin';
 import { queryUserList } from '@/services/plant';
-import { queryProjectMenu, queryUserDetail } from '@/services/SysAdmin';
+import { queryProjectMenu, queryUserDetail, queryRole } from '@/services/SysAdmin';
 
+function getDepUserTree(data) {
+  data.title = `${data.Name}`;
+  data.id = data.ID;
+  data.value = data.ID;
+  // data.selectable = false;
+  if (!data.children) data.children = new Array();
+
+  if (data.children) {
+    data.children.forEach(item => {
+      getDepUserTree(item);
+    });
+  }
+
+  if (data.Users && data.Users.length !== 0) {
+    data.Users.forEach(item => {
+      item.title = item.CName;
+      item.id = item.ID + '||' + data.ID;
+      item.value = item.ID + '||' + data.ID;
+      // item.selectable = true;
+      item.DepId = data.ID;
+      data.children.push(item);
+    });
+  }
+  return data;
+}
+
+const getRoleList = data => {
+  let roleList = [];
+  (data || []).forEach(dep => {
+    (dep.Role || []).forEach(role => {
+      roleList.push(role);
+    });
+
+    if (dep.children) {
+      let res = getRoleList(dep.children, roleList);
+      roleList = res.concat(res);
+    }
+  });
+  return roleList;
+};
 export default {
   namespace: 'user',
 
@@ -19,6 +61,8 @@ export default {
     message: {},
     userList: [],
     depRole: [],
+    depUserTree: [],
+    roleList: [],
   },
 
   effects: {
@@ -48,25 +92,29 @@ export default {
         user.Permissions?.forEach(item => {
           permission = {
             ...permission,
-            ...item.Menus
-          }
-        })
+            ...item.Menus,
+          };
+        });
         try {
           localStorage.setItem('depId', user.DepId);
           if (payload?.ID) {
             const { data: resData } = yield call(queryProjectMenu, payload);
             // permission = resData[0] ? resData[0]?.Menus : {};
-            
           }
         } catch (error) {
           console.error(error);
         }
+        const resRole = yield call(queryUserRole, user.ID);
+        let roleList = getRoleList(resRole.data.Dep);
+        console.log(roleList);
+
         yield put({
           type: 'saveCurrentUser',
           payload: {
             ...user,
             // Permission: {},
             Permission: permission,
+            roleList: roleList,
           },
         });
       }
@@ -107,6 +155,27 @@ export default {
         });
       }
     },
+    *getRoleList({ payload }, { call, put }) {
+      const response = yield call(queryRole, payload);
+      if (response) {
+        yield put({
+          type: 'saveState',
+          payload: { roleList: response.data.list },
+        });
+      }
+    },
+    *fetchDepV2({ payload, callback }, { call, put }) {
+      const response = yield call(queryDepV2, { pageSize: 999999 });
+      if (response) {
+        const depUserTree = response.data.list.map(item => {
+          return getDepUserTree(item);
+        });
+        yield put({
+          type: 'saveState',
+          payload: { depUserTree },
+        });
+      }
+    },
   },
 
   reducers: {
@@ -152,6 +221,12 @@ export default {
         userList: payload,
       };
     },
+    saveState(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
   },
 
   subscriptions: {

+ 1 - 0
src/models/xflow.js

@@ -7,6 +7,7 @@ export default {
     OSSData: {},
     flowDetail: { nodes: [], edges: [] },
     auditList: [],
+    formData: [],
   },
 
   effects: {

+ 64 - 0
src/pages/DDLogin/index.js

@@ -0,0 +1,64 @@
+import React, { useEffect, useState } from 'react';
+import * as dd from 'dingtalk-jsapi';
+import { message } from 'antd';
+import { LoadingOutlined, CheckOutlined } from '@ant-design/icons';
+import { bindDDCode } from '@/services/boom';
+
+function DDLogin(props) {
+  const {
+    match: {
+      params: { dingUserId },
+    },
+  } = props;
+  const [info, setInfo] = useState('');
+  useEffect(() => {
+    if (dd.env.platform != 'notInDingTalk') {
+      dd.ready(function() {
+        dd.runtime.permission.requestAuthCode({
+          corpId: 'ding0cdce2d5dbf986d9', // 企业id
+          onSuccess: async function(info) {
+            setTimeout(() => {
+              setInfo(info.code);
+            }, 500);
+            let res = await bindDDCode(dingUserId, info.code);
+            console.log(res);
+
+            // 通过该免登授权码可以获取用户身份
+          },
+          onFail: error => {
+            setInfo(JSON.stringify(error));
+          },
+        });
+      });
+    }
+  }, []);
+
+  return (
+    <div
+      style={{
+        width: '100vw',
+        fontSize: 24,
+        paddingTop: 50,
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+        flexDirection: 'column',
+      }}
+    >
+      {info ? (
+        <>
+          <CheckOutlined style={{ fontSize: 40, color: '#52c41a' }} />
+          <div>授权成功</div>
+          {/* <div>{info}</div> */}
+        </>
+      ) : (
+        <>
+          <LoadingOutlined style={{ fontSize: 40, color: '#1890ff' }} />
+          <div>授权中...</div>
+        </>
+      )}
+    </div>
+  );
+}
+
+export default DDLogin;

+ 0 - 0
src/pages/Mobile/DataMeter/Alarm.js


+ 118 - 0
src/pages/Mobile/DataMeter/Chart.js

@@ -0,0 +1,118 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { connect } from 'dva';
+import moment from 'moment';
+import style from './Chart.less';
+import { Card, Empty, Spin } from 'antd';
+import { getOptions } from '@/components/ChartUtils/utils';
+const dataCache = {};
+let timer;
+function DataCenter(props) {
+  const [chart, setChart] = useState();
+  // const [timer, setTimer] = useState();
+  const [chartOptions, setChartOptions] = useState({});
+  const chartEle = useRef();
+  const signalRef = useRef();
+  const [loading, setLoading] = useState(false);
+  const {
+    chartConfigList,
+    dispatch,
+    location: {
+      query: { projectId },
+    },
+  } = props;
+  // const { projectId } = props.location; //?
+  // const projectId = 92;
+
+  const handleClickTabs = id => {
+    if (loading) return;
+    let item = chartConfigList.find(c => c.id == id);
+    setLoading(true);
+    setChartOptions({ ...item });
+  };
+
+  useEffect(() => {
+    if (projectId) {
+      // 获取自定义图表列表
+      dispatch({
+        type: 'dataMeterNew/queryConfigList',
+        payload: {
+          projectId,
+        },
+      });
+    }
+  }, [projectId]);
+
+  // useEffect(() => {
+  //   if (!chartOptions) return;
+  //   renderChart();
+  // }, [chartOptions]);
+
+  useEffect(() => {
+    if (chartConfigList && chartConfigList.length > 0) {
+      handleClickTabs(chartConfigList[0].id);
+    }
+  }, [chartConfigList]);
+
+  // const getOptionsForConfig = item => {
+  //   setChartOptions(item);
+  //   clearTimeout(timer);
+  //   let t = setTimeout(() => {
+  //     getOptionsForConfig({ ...item });
+  //   }, 1000 * 60);
+  //   setTimer(t);
+  // };
+
+  const renderChart = async e => {
+    var chartWindow = e.target.contentWindow;
+    let datas = chartOptions.configs;
+    let values = chartOptions.options;
+    let formula = JSON.parse(chartOptions.formula || '[]');
+    if (!datas) return;
+    try {
+      let options = await getOptions(values, datas, formula, projectId);
+      chartWindow.render(options);
+    } catch (error) {
+      console.log(error);
+    }
+    setLoading(false);
+  };
+
+  return (
+    <div className={style.body}>
+      <Card title="数据项" bodyStyle={{ padding: 0 }}>
+        <ul className={style.list}>
+          {(chartConfigList || []).map(item => (
+            <li
+              key={item.id}
+              style={{
+                border: chartOptions.id == item.id ? '1px solid #008dff' : '1px solid #fff',
+              }}
+              onClick={() => handleClickTabs(item.id)}
+            >
+              {item.name}
+            </li>
+          ))}
+        </ul>
+        {chartConfigList.length == 0 && (
+          <Empty style={{ color: '#fff', fontSize: 20, marginTop: 40 }} />
+        )}
+      </Card>
+      <Card title="图表" bodyStyle={{ paddingBottom: 0 }}>
+        <Spin spinning={loading}>
+          {chartOptions.template?.Content && (
+            <iframe
+              style={{ width: '100%', height: 'calc(50vh - 80px)', border: 'none' }}
+              // srcDoc拼上id  防止当模板一致时无法触发onLoad
+              srcDoc={chartOptions?.template?.Content + `<div id="${chartOptions.id}"></div>`}
+              onLoad={renderChart}
+            ></iframe>
+          )}
+        </Spin>
+      </Card>
+    </div>
+  );
+}
+export default connect(({ dataMeterNew, user }) => ({
+  chartConfigList: dataMeterNew.chartConfigList,
+  user: user.currentUser,
+}))(DataCenter);

+ 61 - 0
src/pages/Mobile/DataMeter/Chart.less

@@ -0,0 +1,61 @@
+.body {
+  background-color: #0d1a2b;
+  color: #fff;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  :global {
+    .ant-card {
+      background: transparent;
+      flex: 1;
+    }
+    .ant-card-head {
+      color: #fff;
+    }
+  }
+}
+
+.title {
+  font-size: 20px;
+  padding: 10px;
+}
+
+.listWrapper {
+  height: 40vh;
+  overflow-y: auto;
+}
+
+.list {
+  display: flex;
+  justify-content: space-evenly;
+  flex-wrap: wrap;
+  height: 100%;
+  max-height: calc(50vh - 74px);
+  overflow-y: auto;
+  padding: 10px;
+  margin: 0;
+  li {
+    width: 100%;
+    padding: 6px;
+    text-align: center;
+    color: #fff;
+    font-size: 14px;
+    margin-bottom: 10px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+.listItem {
+  width: 80%;
+  text-align: center;
+  border: 1px solid #fff;
+  border-radius: 5px;
+  margin: 5px 0px;
+  padding: 5px 0px;
+}
+
+.chartWrapper {
+  height: 40vh;
+}

+ 39 - 0
src/pages/Mobile/DataMeter/Craft.js

@@ -0,0 +1,39 @@
+import React from 'react';
+import Flow, { FLOW_TYPE } from '@/components/Flow';
+import { connect } from 'dva';
+import { UnityAction } from '@/utils/utils';
+
+const graphData = JSON.parse(localStorage.graphData || null);
+
+// @connect(({ xflow }) => ({ flowDetail: xflow.flowDetail }))
+class Craft extends React.PureComponent {
+  state = {
+    flowDetail: graphData || { nodes: [], edges: [] },
+  };
+  componentDidMount() {
+    const {
+      dispatch,
+      match: {
+        params: { flowId },
+      },
+    } = this.props;
+
+    UnityAction.on('NODE_SAVE', nodeConfig => {
+      console.log(nodeConfig);
+    });
+  }
+  componentWillUnmount() {
+    UnityAction.off('NODE_SAVE');
+  }
+  render() {
+    const { flowDetail } = this.state;
+
+    return (
+      <div>
+        <Flow meta={{ type: 'edit', flowId: 1 }} flowDetail={flowDetail} />
+      </div>
+    );
+  }
+}
+
+export default Craft;

+ 0 - 0
src/pages/Mobile/DataMeter/Report.js


+ 116 - 120
src/pages/PurchaseAdmin/PurchaseList/Approval/ApprovalModal.js

@@ -1,11 +1,8 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Select, Modal, Input, TreeSelect } from 'antd';
+import { Form, Select, Modal, Input, TreeSelect } from 'antd';
 import moment from 'moment';
 import provinces from './provinces';
 import { queryApproval } from '@/services/approval';
-// import project from '@/pages/ProjectAdmin/ProjectAdmin/models/project';
 const { Option } = Select;
 const { TreeNode } = TreeSelect;
 // 新建
@@ -16,14 +13,16 @@ function AddModal(props) {
     visible,
     onClose,
     onOk,
-    form,
     data,
+    currentUser,
+    depUserTree,
     flowList = [],
     industryList = [],
     typeList = [],
     disabled,
     loading,
   } = props;
+  const [form] = Form.useForm();
   const [codes, setCodes] = useState({
     type: '',
     industry: '',
@@ -34,51 +33,28 @@ function AddModal(props) {
   const [type, setType] = useState({});
 
   const handleOk = () => {
-    form.validateFields((err, fieldsValue) => {
-      if (err) return;
+    form.validateFields().then(fieldsValue => {
       let values = { ...fieldsValue, id: data.id };
-      //项目分类为研发时
-      if (fieldsValue.type_id == 7) {
-        values.project_name = fieldsValue.project_name;
-        values.flow_id = Number(fieldsValue.flow_id);
-        values.type_id = Number(fieldsValue.type_id);
-        // 获得flow下第一个node的id
-        values.node_id = flowList.find(item => item.id == values.flow_id).Nodes[0].id;
-        //以下为测试用默认值
-        // values.industry_id = 0;
-        // values.location = '';
-        // values.location_code = '';
-        // values.project_full_code = '';
-        // values.name = '';
-        // values.version = '';
-
-        //通过当前项目数生成项目编号
-        // dispatch({
-        //   type: 'approval/queryApproval',
-        //   callback: data => {
-        //     let project_full_code = '';
-        //     if (total < 10) {
-        //       project_full_code = '00' + data.pagination.total;
-        //     } else if (total < 100) project_full_code = '0' + data.pagination.total;
-        //     else project_full_code = data.pagination.total;
-        //     values.project_full_code = `RD${moment().format('YYYYMM')}${project_full_code}`;
-        //     onOk(values);
-        //   },
-        // });
-        onOk(values);
-      } else {
-        values.project_name = fieldsValue.project_name;
-        values.type_id = Number(fieldsValue.type_id);
+      values.project_name = fieldsValue.project_name;
+      values.flow_id = Number(fieldsValue.flow_id);
+      values.type_id = Number(fieldsValue.type_id);
+      // 获得flow下第一个node的id
+      values.node_id = flowList.find(item => item.id == values.flow_id).Nodes[0].id;
+      //项目分类为不为研发时
+      if (fieldsValue.type_id != 7) {
         values.industry_id = Number(fieldsValue.industry_id);
-        values.flow_id = Number(fieldsValue.flow_id);
-        // 获得flow下第一个node的id
-        values.node_id = flowList.find(item => item.id == values.flow_id).Nodes[0].id;
         let [location, location_code] = fieldsValue.location.split('##');
         values.location = location;
         values.location_code = location_code;
         values.project_full_code = `${codes.type}${codes.industry}${codes.location}${codes.name}${codes.version}`;
-        onOk(values);
       }
+      if (fieldsValue.author) {
+        values.author = Number(fieldsValue.author.split('||')[0]);
+        values.author_dep_id = Number(fieldsValue.author.split('||')[1]);
+      } else {
+        values.author = null;
+      }
+      onOk(values);
     });
   };
 
@@ -88,7 +64,6 @@ function AddModal(props) {
       let code = item.code || '';
       if (code.length == 4) {
         code = code.substr(1);
-        console.log(code);
       }
       if (code) {
         title += `(${code})`;
@@ -108,7 +83,7 @@ function AddModal(props) {
       type: item.code,
     });
     setType(item);
-    form.setFieldsValue({ flow_id: id == 7 ? '4' : '1'});
+    form.setFieldsValue({ flow_id: id == 7 ? '4' : '1' });
   };
   const changeIndustry = id => {
     const item = industryList.find(item => item.id == id);
@@ -133,7 +108,7 @@ function AddModal(props) {
   const onBlurName = e => {
     let value = e.target.value.toUpperCase();
     while (value.length < 3) {
-      value = value + "V";
+      value = value + 'V';
     }
     form.setFieldsValue({
       name: value,
@@ -145,12 +120,14 @@ function AddModal(props) {
   };
 
   const renderDetail = () => {
-    return <>
-      <Form.Item label="行业名称">
-        {form.getFieldDecorator('industry_id', {
-          initialValue: String(data.industry_id || ''),
-          rules: [{ required: true, message: '请选择行业名称' }],
-        })(
+    return (
+      <>
+        <Form.Item
+          label="行业名称"
+          name="industry_id"
+          initialValue={String(data.industry_id || '')}
+          rules={[{ required: true, message: '请选择行业名称' }]}
+        >
           <Select style={{ width: '100%' }} onChange={changeIndustry}>
             {industryList.map(item => (
               <Option key={item.id}>
@@ -158,55 +135,54 @@ function AddModal(props) {
               </Option>
             ))}
           </Select>
-        )}
-      </Form.Item>
-      <Form.Item label="项目地区">
-        {form.getFieldDecorator('location', {
-          initialValue: data.location,
-          rules: [{ required: true, message: '请选择项目地区' }],
-        })(
+        </Form.Item>
+        <Form.Item
+          label="项目地区"
+          name="location"
+          initialValue={data.location}
+          rules={[{ required: true, message: '请选择项目地区' }]}
+        >
           <TreeSelect
             dropdownStyle={{ maxHeight: 300, overflow: 'auto' }}
             onChange={changeLocation}
           >
             {renderTreeNodes(provinces)}
           </TreeSelect>
-        )}
-      </Form.Item>
-      <Form.Item label="项目简称">
-        {form.getFieldDecorator('name', {
-          initialValue: data.name,
-          rules: [
+        </Form.Item>
+        <Form.Item
+          label="项目简称"
+          name="name"
+          initialValue={data.name}
+          rules={[
             { required: true, message: '请输入项目简称' },
             {
               validator: (rule, value, callback) => {
-                if (value.match(/[^A-Za-z]/g)) {
-                  callback('项目简称只能是英文字符');
-                } else {
-                  callback();
-                }
+                if (value.match(/[^A-Za-z]/g)) callback('项目简称只能是英文字符');
+                else callback();
               },
             },
-          ],
-        })(<Input maxLength={3} onBlur={onBlurName} />)}
-      </Form.Item>
-      <Form.Item label="项目期数">
-        {form.getFieldDecorator('version', {
-          initialValue: data.version,
-          rules: [{ required: true, message: '请选择项目期数' }],
-        })(
+          ]}
+        >
+          <Input maxLength={3} onBlur={onBlurName} />
+        </Form.Item>
+        <Form.Item
+          label="项目期数"
+          name="version"
+          initialValue={data.version}
+          rules={[{ required: true, message: '请选择项目期数' }]}
+        >
           <Select style={{ width: '100%' }} onChange={changeVersion}>
             {['一期', '二期', '三期', '四期', '五期'].map((item, index) => (
               <Option key={index + 1}>{item}</Option>
             ))}
           </Select>
-        )}
-      </Form.Item>
-      <Form.Item label="项目编号">
-        {codes.type || '***'}-{codes.industry || '***'}-{codes.location || '***'}-
-        {codes.name || '***'}-{codes.version || '*'}
-      </Form.Item>
-    </>;
+        </Form.Item>
+        <Form.Item label="项目编号">
+          {codes.type || '***'}-{codes.industry || '***'}-{codes.location || '***'}-
+          {codes.name || '***'}-{codes.version || '*'}
+        </Form.Item>
+      </>
+    );
   };
 
   // const renderCode = () => {
@@ -253,46 +229,66 @@ function AddModal(props) {
       onCancel={onClose}
       onOk={handleOk}
     >
-      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
-        <Form.Item label="项目名称">
-          {form.getFieldDecorator('project_name', {
-            initialValue: String(data.project_name || ''),
-            rules: [{ required: true, message: '请输入项目名称' }],
-          })(
-            <Input style={{ width: '100%' }}/>
-          )}
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label="项目名称"
+          name="project_name"
+          initialValue={String(data.project_name || '')}
+          rules={[{ required: true, message: '请输入项目名称' }]}
+        >
+          <Input style={{ width: '100%' }} />
         </Form.Item>
-        <Form.Item label="项目类别">
-          {form.getFieldDecorator('type_id', {
-            initialValue: String(data.type_id || ''),
-            rules: [{ required: true, message: '请选择项目类别' }],
-          })(
-            <Select style={{ width: '100%' }} onChange={changeType}>
-              {typeList.map(item => (
-                <Option key={item.id}>
-                  {item.name}({item.code})
-                </Option>
-              ))}
-            </Select>
-          )}
+        <Form.Item
+          label="项目类别"
+          name="type_id"
+          initialValue={String(data.type_id || '')}
+          rules={[{ required: true, message: '请选择项目类别' }]}
+        >
+          <Select style={{ width: '100%' }} onChange={changeType}>
+            {typeList.map(item => (
+              <Option key={item.id}>
+                {item.name}({item.code})
+              </Option>
+            ))}
+          </Select>
         </Form.Item>
-        <Form.Item label="流程">
-          {form.getFieldDecorator('flow_id', {
-            initialValue: String(data.flow_id || ''),
-            rules: [{ required: true, message: '请选择流程' }],
-          })(
-            <Select style={{ width: '100%' }} disabled>
-              {flowList
-                .filter(item => item && (item.id != 2 && item.id != 3))
-                .map(item => (
-                  <Option key={item.id}>{item.name}</Option>
-                ))}
-            </Select>
-          )}
+        <Form.Item
+          label="流程"
+          name="flow_id"
+          initialValue={String(data.flow_id || '')}
+          rules={[{ required: true, message: '请选择流程' }]}
+        >
+          <Select style={{ width: '100%' }} disabled>
+            {flowList
+              .filter(item => item && item.id != 2 && item.id != 3)
+              .map(item => (
+                <Option key={item.id}>{item.name}</Option>
+              ))}
+          </Select>
         </Form.Item>
+        {currentUser.IsSuper && (
+          <Form.Item
+            label="售前经理"
+            name="author"
+            initialValue={String(
+              data.author && data.author_dep_id ? `${data.author}||${data.author_dep_id}` : ''
+            )}
+          >
+            <TreeSelect
+              showSearch
+              allowClear
+              style={{ width: '100%' }}
+              multiple={false}
+              filterTreeNode={(input, option) => {
+                return option.props.title === input;
+              }}
+              treeData={depUserTree}
+            />
+          </Form.Item>
+        )}
         {type?.id != 7 && renderDetail()}
       </Form>
     </Modal>
   );
 }
-export default Form.create()(AddModal);
+export default AddModal;

+ 77 - 71
src/pages/PurchaseAdmin/PurchaseList/Approval/Auth.js

@@ -1,7 +1,5 @@
 import React, { useState, useEffect, useMemo } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Table, Button, Divider, Modal, Popover, Input, Select } from 'antd';
+import { Table, Button, Form, Divider, Modal, Popover, Input, Select } from 'antd';
 import moment from 'moment';
 import router from 'umi/router';
 import styles from './List.less';
@@ -21,20 +19,19 @@ const STATUS = [
     value: 1,
     label: '转执行',
   },
+  {
+    value: 2,
+    label: '转运营',
+  },
+  {
+    value: 3,
+    label: '转质保',
+  },
 ];
 
 function Auth(props) {
-  const {
-    industryList,
-    typeList,
-    data,
-    flowList,
-    currentUser,
-    depRole,
-    dispatch,
-    loading,
-    form,
-  } = props;
+  const { industryList, typeList, data, flowList, currentUser, depRole, dispatch, loading } = props;
+  const [form] = Form.useForm();
   const [visible, setVisible] = useState(false);
   const [detailVisible, setDetailVisible] = useState(false);
   const [rejectVisible, setRejectVisible] = useState(false);
@@ -51,7 +48,7 @@ function Auth(props) {
     {
       title: '分类',
       dataIndex: 'TypeInfo',
-      render: TypeInfo => TypeInfo ?`${TypeInfo.name}(${TypeInfo.code})`: "-",
+      render: TypeInfo => (TypeInfo ? `${TypeInfo.name}(${TypeInfo.code})` : '-'),
     },
     /*
     {
@@ -76,22 +73,24 @@ function Auth(props) {
     */
     {
       title: '流程',
-      dataIndex: 'FlowInfo.name',
+      dataIndex: ['FlowInfo', 'name'],
     },
     {
       title: '状态',
       dataIndex: 'project_status',
       render: project_status => {
-        return project_status === 0 ? <>售前</> : <>转执行</>;
+        // return project_status === 0 ? <>售前</> : <>转执行</>;
         //若添加其他状态则启用以下switch case:
-        /*
-        switch (project_status){
+        switch (project_status) {
           case 0:
-            return <>{'售前'}</>;
+            return <>售前</>;
           case 1:
-            return <>{'转执行'}</>;
+            return <>转执行</>;
+          case 2:
+            return <>转运营</>;
+          case 3:
+            return <>转质保</>;
         }
-        */
       },
     },
     {
@@ -125,31 +124,33 @@ function Auth(props) {
       },
     },
     {
-      title: '创建人',
-      dataIndex: 'AuthorUser.CName',
+      title: '售前项目经理',
+      dataIndex: 'AuthorUser',
+      render: AuthorUser => (AuthorUser ? AuthorUser.CName : '-'),
     },
     {
       title: '创建时间',
       dataIndex: 'c_time',
       render: c_time => moment(c_time).format('YYYY.MM.DD'),
     },
+    {
+      title: '执行经理',
+      dataIndex: 'Leader',
+      render: Leader => (Leader ? Leader.CName : '-'),
+    },
     {
       title: '操作',
       render: record => (
         <>
-          {record.type_id != 7 && (
-            <>
-              <a
-                onClick={() => {
-                  setCurrentItem(record);
-                  setDetailVisible(true);
-                }}
-              >
-                项目详情
-              </a>
-              <Divider type="vertical" />
-            </>
-          )}
+          <a
+            onClick={() => {
+              setCurrentItem(record);
+              setDetailVisible(true);
+            }}
+          >
+            项目详情
+          </a>
+          <Divider type="vertical" />
           <a
             onClick={() => {
               setCurrentItem(record);
@@ -164,11 +165,15 @@ function Auth(props) {
   ];
 
   const canAuth = useMemo(() => {
-    let { NodeInfo, audit_status } = currentItem;
+    let { NodeInfo, audit_status, project_status } = currentItem;
     if (!NodeInfo || flowList.length == 0 || depRole.length == 0) return;
     if (audit_status != 1) return;
     let flow = flowList.find(item => item.id == NodeInfo.flow_id);
     if (!flow) return false;
+
+    if (project_status == 2) return currentItem.opt_manager_id == currentUser.ID;
+    if (project_status == 3) return currentItem.wty_manager_id == currentUser.ID;
+
     let { NodeAudits } = flow.Nodes.find(item => item.id == NodeInfo.id);
 
     const role = depRole.find(item => {
@@ -229,45 +234,46 @@ function Auth(props) {
   };
 
   const handleSearch = () => {
-    form.validateFields((error, { projectCode, projectStatus }) => {
-      // console.log(error,values);
-      let params = {};
-      params.project_code = projectCode;
-      params.project_status = projectStatus;
+    const { projectName, projectCode, projectStatus } = form.getFieldsValue();
+    // console.log(error,values);
+    let params = {};
+    params.project_name = projectName;
+    params.project_code = projectCode?.toUpperCase();
+    params.project_status = projectStatus;
 
-      dispatch({
-        type: 'approval/queryAuth',
-        payload: params,
-      });
+    dispatch({
+      type: 'approval/queryAuth',
+      payload: params,
     });
   };
 
   const renderSearch = () => {
     return (
-      <Form style={{ marginBottom: 20 }} layout="inline">
-        <Form.Item label="项目编号">
-          {form.getFieldDecorator('projectCode', {
-            initialValue: null,
-          })(<Input style={{ width: 200 }}></Input>)}
+      <Form
+        style={{ marginBottom: 20 }}
+        layout="inline"
+        initialValues={{ projectName: null, projectCode: null, projectStatus: null }}
+        form={form}
+      >
+        <Form.Item label="项目名称" name="projectName">
+          <Input style={{ width: 200 }} />
         </Form.Item>
-        <Form.Item label="状态">
-          {form.getFieldDecorator('projectStatus', {
-            initialValue: null,
-          })(
-            <Select
-              allowClear
-              showSearch
-              style={{ width: 120 }}
-              filterOption={(input, option) =>
-                option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-              }
-            >
-              <Option value={null}>全部</Option>
-              {STATUS.map(item => (
-                <Option key={item.value}>{item.label}</Option>
-              ))}
-            </Select>
-          )}
+        <Form.Item label="项目编号" name="projectCode">
+          <Input style={{ width: 200 }} />
+        </Form.Item>
+        <Form.Item label="状态" name="projectStatus">
+          <Select
+            showSearch
+            style={{ width: 120 }}
+            filterOption={(input, option) =>
+              option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }
+          >
+            <Option value={null}>全部</Option>
+            {STATUS.map(item => (
+              <Option key={item.value}>{item.label}</Option>
+            ))}
+          </Select>
         </Form.Item>
         <Form.Item>
           <Button type="primary" loading={loading} onClick={handleSearch}>
@@ -337,4 +343,4 @@ export default connect(({ approval, user, loading }) => ({
   currentUser: user.currentUser,
   depRole: user.depRole,
   loading: loading.models.approval,
-}))(Form.create()(Auth));
+}))(Auth);

+ 18 - 9
src/pages/PurchaseAdmin/PurchaseList/Approval/AuthModal.js

@@ -1,25 +1,34 @@
 import React, { useState, useEffect, useMemo } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Button, Modal, Steps } from 'antd';
+import { Button, Form, Modal, Steps } from 'antd';
 
 const { Step } = Steps;
 // 新建
 function AuthModal(props) {
   const { visible, onClose, onAuth, form, data, flowList = [], canAuth, loading } = props;
 
-  const folw = useMemo(() => {
+  const flow = useMemo(() => {
     if (!data.flow_id) return {};
     return flowList.find(item => item.id == data.flow_id) || {};
   }, [flowList, data]);
 
   const current = useMemo(() => {
     if (!data.node_id) return 0;
-    return folw.Nodes?.findIndex(item => item.id == data.node_id);
+    return flow.Nodes?.findIndex(item => item.id == data.node_id);
   }, [flowList, data]);
 
   const getAudits = nodeInfo => {
-    return (nodeInfo || []).map(item => item.AuthorRoleInfo.Name).join(',');
+    switch (nodeInfo.id) {
+      case 11:
+        return '执行项目经理';
+      case 12:
+        return '运营经理';
+      case 13:
+        return '执行项目经理';
+      case 14:
+        return '质保经理';
+      default:
+        return (nodeInfo.NodeAudits || []).map(item => item.AuthorRoleInfo.Name).join(',');
+    }
   };
 
   const renderFooter = () => {
@@ -42,7 +51,7 @@ function AuthModal(props) {
 
   return (
     <Modal
-      title="项目立项"
+      title="审核详情"
       width={800}
       visible={visible}
       onCancel={onClose}
@@ -50,11 +59,11 @@ function AuthModal(props) {
     >
       <Steps current={current}>
         {/* <Steps current={data?.node_id}> */}
-        {(folw.Nodes || []).map(item => (
+        {(flow.Nodes || []).map(item => (
           <Step
             key={item.id}
             title={item.node}
-            description={`审批人:${getAudits(item.NodeAudits)}`}
+            description={`审批人:${getAudits(item)}`}
           />
         ))}
       </Steps>

+ 102 - 24
src/pages/PurchaseAdmin/PurchaseList/Approval/DetailModal.js

@@ -1,7 +1,8 @@
-import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Modal } from 'antd';
+import React, { useState, useEffect, useMemo } from 'react';
+import { Form, Modal, Steps } from 'antd';
+import styles from './DetailModal.less';
+
+const { Step } = Steps;
 // 新建
 function DetailModal(props) {
   const { visible, onClose, onOk, form, data, flowList = [], disabled, loading } = props;
@@ -24,31 +25,108 @@ function DetailModal(props) {
     });
   }, [data, visible]);
 
-  return (
-    <Modal title="项目立项" visible={visible} onCancel={onClose} footer={null}>
-      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
-        <Form.Item label="项目名称">{data.project_name}</Form.Item>
-        {data.TypeInfo && <Form.Item label="项目类别">{data.TypeInfo?.name}</Form.Item>}
-        {data.IndustryInfo && <Form.Item label="行业名称">{data.IndustryInfo?.name}</Form.Item>}
-        <Form.Item label="流程">{flowList.find(item => item.id == data.flow_id)?.name}</Form.Item>
-        {data.location && (
-          <Form.Item label="项目地区">
-            {data.location}({data.location_code})
+  const renderDetail = () => (
+    <>
+      <div className={styles.subTitle}>项目详情</div>
+      <Form labelCol={{ span: 4 }} wrapperCol={{ span: 18 }}>
+        <Form.Item className={styles.formItem} label="项目名称">
+          {data.project_name}
+        </Form.Item>
+        {data.TypeInfo && (
+          <Form.Item className={styles.formItem} label="项目类别">
+            {data.TypeInfo?.name}
           </Form.Item>
         )}
-        {data.name && <Form.Item label="项目简称">{data.name}</Form.Item>}
-
-        {data.version && <Form.Item label="项目批次">{data.version}期</Form.Item>}
-
-        <Form.Item label="审核人">{data.AuthorUser?.CName}</Form.Item>
-        <Form.Item label="所属部门">{data.AuthorDepInfo?.Name}</Form.Item>
-        {codes.type && (
-          <Form.Item label="项目编号">
-            {codes.type || '***'}-{codes.industry || '***'}-{codes.location || '***'}-
-            {codes.name || '***'}-{codes.version || '*'}
+        <Form.Item className={styles.formItem} label="流程">
+          {flowList.find(item => item.id == data.flow_id)?.name}
+        </Form.Item>
+        {data.type_id != 7 && (
+          <>
+            <Form.Item className={styles.formItem} label="行业名称">
+              {data.IndustryInfo?.name}
+            </Form.Item>
+            <Form.Item className={styles.formItem} label="项目地区">
+              {data.location}({data.location_code})
+            </Form.Item>
+            <Form.Item className={styles.formItem} label="项目简称">
+              {data.name}
+            </Form.Item>
+            <Form.Item className={styles.formItem} label="项目批次">
+              {data.version}期
+            </Form.Item>
+          </>
+        )}
+        {data.AuthorUser && (
+          <Form.Item className={styles.formItem} label="售前项目经理">
+            {data.AuthorUser.CName}
+          </Form.Item>
+        )}
+        {data.AuthorDepInfo && (
+          <Form.Item className={styles.formItem} label="所属部门">
+            {data.AuthorDepInfo.Name}
+          </Form.Item>
+        )}
+        {data.project_full_code && (
+          <Form.Item className={styles.formItem} label="项目编号">
+            {data.project_full_code}
+          </Form.Item>
+        )}
+        {data.WtyManager && (
+          <Form.Item className={styles.formItem} label="质保经理">
+            {data.WtyManager.CName}
+          </Form.Item>
+        )}
+        {data.OptManager && (
+          <Form.Item className={styles.formItem} label="运营经理">
+            {data.OptManager.CName}
           </Form.Item>
         )}
       </Form>
+    </>
+  );
+
+  const flow = useMemo(() => {
+    if (!data.flow_id) return {};
+    return flowList.find(item => item.id == data.flow_id) || {};
+  }, [flowList, data]);
+
+  const current = useMemo(() => {
+    if (!data.node_id) return 0;
+    return flow.Nodes?.findIndex(item => item.id == data.node_id);
+  }, [flowList, data]);
+
+  const getAudits = nodeInfo => {
+    switch (nodeInfo.id) {
+      case 11:
+        return '执行项目经理';
+      case 12:
+        return '运营经理';
+      case 13:
+        return '执行项目经理';
+      case 14:
+        return '质保经理';
+      default:
+        return (nodeInfo.NodeAudits || []).map(item => item.AuthorRoleInfo.Name).join(',');
+    }
+  };
+
+  const renderAuth = () => (
+    <div className={styles.authDetail}>
+      <div className={styles.subTitle}>审核详情</div>
+      <Steps className={styles.auth} current={current}>
+        {/* <Steps current={data?.node_id}> */}
+        {(flow.Nodes || []).map(item => (
+          <Step key={item.id} title={item.node} description={`审批人:${getAudits(item)}`} />
+        ))}
+      </Steps>
+    </div>
+  );
+
+  return (
+    <Modal title="项目详情" width={800} visible={visible} onCancel={onClose} footer={null}>
+      {/* {data.type_id != 7 && renderDetail()} */}
+      {renderDetail()}
+      {data.audit_status != 0 && renderAuth()}
     </Modal>
   );
 }

+ 16 - 0
src/pages/PurchaseAdmin/PurchaseList/Approval/DetailModal.less

@@ -0,0 +1,16 @@
+.authDetail {
+  margin-top: 20px;
+  .auth {
+    padding-top: 10px;
+  }
+}
+
+.subTitle {
+  font-size: 20px;
+  font-weight: bold;
+  margin-bottom: 10px;
+}
+
+.formItem {
+  margin-bottom: 0;
+}

+ 35 - 74
src/pages/PurchaseAdmin/PurchaseList/Approval/ExecutionModal.js

@@ -1,18 +1,16 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Select, Modal, Input, TreeSelect } from 'antd';
-import { connect } from 'dva'
+import { Form, Select, Modal, Input, TreeSelect } from 'antd';
+import { connect } from 'dva';
 const { Option } = Select;
 const { TreeNode } = TreeSelect;
 
 function ExecutionModal(props) {
-  const { visible, onOk, onClose, form, currentItem, flowList = [], loading, depUserTree, dispatch } = props;
+  const { visible, onOk, onClose, currentItem, loading, depUserTree, dispatch } = props;
+  const [form] = Form.useForm();
 
   const handleOk = () => {
-    form.validateFields((err, {managerID, contractStatus}) => {
-      if (err) return;
-      const [exe_manager_id,dep_id] = managerID.split("||")
+    form.validateFields().then(({ managerID, contractStatus }) => {
+      const [exe_manager_id, dep_id] = managerID.split('||');
       dispatch({
         type: 'approval/startExecution',
         payload: {
@@ -22,40 +20,10 @@ function ExecutionModal(props) {
           exe_manager_id: Number(exe_manager_id),
         },
         callback: () => onOk(),
-      })
+      });
     });
   };
-
-  const onChangeManager = (id,e) => {
-    console.log(id)
-  }
-
-  const renderUserSelectTreeNodes = data => {
-    return data.map(item => {
-      if (item.children) {
-        return (
-          <TreeNode
-            title={item.title}
-            key={item.key}
-            value={item.manage || item.value}
-            dataRef={item}
-            selectable={item.selectable}
-          >
-            {renderUserSelectTreeNodes(item.children)}
-          </TreeNode>
-        );
-      }
-      return (
-        <TreeNode
-          title={item.title}
-          key={item.ID}
-          value={item.manage || item.value}
-          selectable={item.selectable}
-        />
-      );
-    });
-  };
-
+  
   return (
     <Modal
       title="转执行"
@@ -66,44 +34,37 @@ function ExecutionModal(props) {
       onCancel={onClose}
       onOk={handleOk}
     >
-      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
-        <Form.Item label="项目经理">
-          {form.getFieldDecorator('managerID', {
-            rules: [
-              {
-                required: true,
-                message: '请选择项目经理',
-              },
-            ],
-          })(
-            <TreeSelect
-              showSearch
-              allowClear
-              style={{ width: '100%' }}
-              placeholder="请选择项目经理"
-              multiple={false}
-              filterTreeNode={(input, option) => {
-                return option.props.title === input;
-              }}
-              onChange={onChangeManager}
-            >
-              {renderUserSelectTreeNodes(depUserTree)}
-            </TreeSelect>
-          )}
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label="项目经理"
+          name="managerID"
+          rules={[{ required: true, message: '请选择项目经理' }]}
+        >
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: '100%' }}
+            placeholder="请选择项目经理"
+            multiple={false}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+            treeData={depUserTree}
+          />
         </Form.Item>
 
-        <Form.Item label="合同状态">
-          {form.getFieldDecorator('contractStatus', {
-            rules: [{ required: true, message: '请选择合同状态' }],
-          })(
-            <Select style={{ width: '100%' }}>
-              <Option key={0}>无合同</Option>
-              <Option key={1}>有合同</Option>
-            </Select>
-          )}
+        <Form.Item
+          label="合同状态"
+          name="contractStatus"
+          rules={[{ required: true, message: '请选择合同状态' }]}
+        >
+          <Select style={{ width: '100%' }}>
+            <Option key={0}>无合同</Option>
+            <Option key={1}>有合同</Option>
+          </Select>
         </Form.Item>
       </Form>
     </Modal>
   );
 }
-export default connect()(Form.create()(ExecutionModal));
+export default connect()(ExecutionModal);

+ 113 - 64
src/pages/PurchaseAdmin/PurchaseList/Approval/List.js

@@ -1,7 +1,5 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Table, Button, Select, Divider, Modal, Popover, Input, Checkbox } from 'antd';
+import { Table, Button, Form, Select, Divider, Modal, Popover, Input, Checkbox } from 'antd';
 import moment from 'moment';
 import router from 'umi/router';
 import styles from './List.less';
@@ -23,6 +21,14 @@ const STATUS = [
     value: 1,
     label: '转执行',
   },
+  {
+    value: 2,
+    label: '转运营',
+  },
+  {
+    value: 3,
+    label: '转质保',
+  },
 ];
 
 function List(props) {
@@ -34,10 +40,10 @@ function List(props) {
     currentUser,
     dispatch,
     loading,
-    form,
     depUserTree,
     member,
   } = props;
+  const [form] = Form.useForm();
   const [addVisible, setAddVisible] = useState(false);
   const [detailVisible, setDetailVisible] = useState(false);
   const [executionVisible, setExecutionVisible] = useState(false);
@@ -83,22 +89,24 @@ function List(props) {
     */
     {
       title: '流程',
-      dataIndex: 'FlowInfo.name',
+      dataIndex: ['FlowInfo', 'name'],
     },
     {
       title: '状态',
       dataIndex: 'project_status',
       render: project_status => {
-        return project_status === 0 ? <>售前</> : <>转执行</>;
+        // return project_status === 0 ? <>售前</> : <>转执行</>;
         //若添加其他状态则启用以下switch case:
-        /*
-        switch (project_status){
+        switch (project_status) {
           case 0:
             return <>售前</>;
           case 1:
             return <>转执行</>;
+          case 2:
+            return <>转运营</>;
+          case 3:
+            return <>转质保</>;
         }
-        */
       },
     },
     {
@@ -132,14 +140,20 @@ function List(props) {
       },
     },
     {
-      title: '创建人',
-      dataIndex: 'AuthorUser.CName',
+      title: '售前项目经理',
+      dataIndex: 'AuthorUser',
+      render: AuthorUser => (AuthorUser ? AuthorUser.CName : '-'),
     },
     {
       title: '创建时间',
       dataIndex: 'c_time',
       render: c_time => moment(c_time).format('YYYY.MM.DD'),
     },
+    {
+      title: '执行经理',
+      dataIndex: 'Leader',
+      render: Leader => (Leader ? Leader.CName : '-'),
+    },
     {
       title: '操作',
       render: record => renderEditBtns(record),
@@ -147,17 +161,16 @@ function List(props) {
   ];
 
   const handleSearch = () => {
-    form.validateFields((error, { projectCode, projectStatus }) => {
-      // console.log(error,values);
-      let params = {};
-      params.project_code = projectCode;
-      params.project_status = projectStatus;
-      params.currentPage = 1;
+    const { projectName, projectCode, projectStatus } = form.getFieldsValue();
+    let params = {};
+    params.project_name = projectName;
+    params.project_code = projectCode?.toUpperCase();
+    params.project_status = projectStatus;
+    params.currentPage = 1;
 
-      dispatch({
-        type: 'approval/queryApproval',
-        payload: params,
-      });
+    dispatch({
+      type: 'approval/queryApproval',
+      payload: params,
     });
   };
 
@@ -176,30 +189,30 @@ function List(props) {
 
   const renderSearch = () => {
     return (
-      <Form layout="inline">
-        <Form.Item label="项目编号">
-          {form.getFieldDecorator('projectCode', {
-            initialValue: null,
-          })(<Input style={{ width: 200 }}></Input>)}
+      <Form
+        form={form}
+        layout="inline"
+        initialValues={{ projectName: null, projectCode: null, projectStatus: null }}
+      >
+        <Form.Item label="项目名称" name="projectName">
+          <Input style={{ width: 200 }} />
+        </Form.Item>
+        <Form.Item label="项目编号" name="projectCode">
+          <Input style={{ width: 200 }} />
         </Form.Item>
-        <Form.Item label="状态">
-          {form.getFieldDecorator('projectStatus', {
-            initialValue: null,
-          })(
-            <Select
-              allowClear
-              showSearch
-              style={{ width: 120 }}
-              filterOption={(input, option) =>
-                option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-              }
-            >
-              <Option value={null}>全部</Option>
-              {STATUS.map(item => (
-                <Option key={item.value}>{item.label}</Option>
-              ))}
-            </Select>
-          )}
+        <Form.Item label="状态" name="projectStatus">
+          <Select
+            showSearch
+            style={{ width: 120 }}
+            filterOption={(input, option) =>
+              option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }
+          >
+            <Option value={null}>全部</Option>
+            {STATUS.map(item => (
+              <Option key={item.value}>{item.label}</Option>
+            ))}
+          </Select>
         </Form.Item>
         <Form.Item>
           <Button type="primary" loading={loading} onClick={handleSearch}>
@@ -361,39 +374,75 @@ function List(props) {
       </>
     );
     let { audit_status, project_status, type_id } = record;
+    //权限审核
+    let canEdit = () => {
+      if (currentUser.IsSuper) return true;
+      switch (audit_status) {
+        case 0:
+          return currentUser.ID == record.author;
+        case 1:
+          return false;
+        case 2:
+          if (project_status == 0) return currentUser.ID == record.author;
+          if (project_status == 1) return currentUser.ID == record.LeaderId;
+          return false;
+        case 3:
+          switch (project_status) {
+            case 0:
+              return currentUser.ID == record.author;
+            case 1:
+              return currentUser.ID == record.LeaderId;
+            case 2:
+              return currentUser.ID == record.LeaderId || currentUser.ID == record.opt_manager_id;
+            case 3:
+              return currentUser.ID == record.LeaderId || currentUser.ID == record.wty_manager_id;
+          }
+          return false;
+      }
+    };
     let toReturn = [];
-    if (type_id != 7) dividerPush(detailBtn, toReturn);
+    dividerPush(detailBtn, toReturn);
     switch (audit_status) {
       //未提交
       case 0:
-        if (currentUser.ID == record.author || currentUser.IsSuper) dividerPush(editBtn, toReturn);
+        canEdit() && dividerPush(editBtn, toReturn);
         break;
       //审核中
       case 1:
         break;
       //审核拒绝
       case 2:
-        if (project_status == 0 && (currentUser.ID == record.author || currentUser.IsSuper))
-          dividerPush(editBtn, toReturn);
-        else if (
-          project_status == 1 &&
-          (currentUser.ID == record.LeaderId || currentUser.IsSuper)
-        ) {
+        if (project_status == 0 && canEdit()) dividerPush(editBtn, toReturn);
+        else if (project_status == 1 && canEdit()) {
           dividerPush(memberBtn, toReturn);
           dividerPush(statusBtn, toReturn);
         }
         break;
       //审核通过
       case 3:
-        if (project_status == 0 && (currentUser.ID == record.author || currentUser.IsSuper)) {
-          dividerPush(memberBtn, toReturn);
-          dividerPush(executionBtn, toReturn);
-        } else if (
-          project_status == 1 &&
-          (currentUser.ID == record.LeaderId || currentUser.IsSuper)
-        ) {
-          dividerPush(memberBtn, toReturn);
-          dividerPush(statusBtn, toReturn);
+        switch (project_status) {
+          //售前
+          case 0:
+            if (canEdit()) {
+              dividerPush(memberBtn, toReturn);
+              dividerPush(executionBtn, toReturn);
+            }
+            break;
+          //转执行
+          case 1:
+            if (canEdit()) {
+              dividerPush(memberBtn, toReturn);
+              dividerPush(statusBtn, toReturn);
+            }
+            break;
+          //转运营
+          case 2:
+            canEdit() && dividerPush(memberBtn, toReturn);
+            break;
+          //转质保
+          case 3:
+            canEdit() && dividerPush(memberBtn, toReturn);
+            break;
         }
         break;
     }
@@ -443,6 +492,8 @@ function List(props) {
         onChange={queryList}
       />
       <ApprovalModal
+        currentUser={currentUser}
+        depUserTree={depUserTree}
         loading={loading}
         industryList={industryList}
         flowList={flowList}
@@ -464,7 +515,6 @@ function List(props) {
       <ExecutionModal
         depUserTree={depUserTree}
         loading={loading}
-        flowList={flowList}
         visible={executionVisible}
         currentItem={currentItem}
         onOk={() => setExecutionVisible(false)}
@@ -481,7 +531,6 @@ function List(props) {
       <QualityOperateModal
         depUserTree={depUserTree}
         loading={loading}
-        flowList={flowList}
         visible={qualityOperateVisible}
         currentItem={currentItem}
         onOk={() => setQualityOperateVisible(false)}
@@ -501,4 +550,4 @@ export default connect(({ approval, user, loading }) => ({
   loading: loading.models.approval,
   depUserTree: approval.depUserTree,
   member: approval.member,
-}))(Form.create()(List));
+}))(List);

+ 33 - 81
src/pages/PurchaseAdmin/PurchaseList/Approval/MemberModal.js

@@ -1,40 +1,11 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Modal, TreeSelect, Table, Button } from 'antd';
+import { Form, Modal, TreeSelect, Table, Button, Input } from 'antd';
 import { connect } from 'dva';
 const { TreeNode } = TreeSelect;
 
-//测试用临时数据
-const tempData = [
-  {
-    name: 'aaa',
-    phone: '111',
-    email: 'aaa@a.com',
-  },
-  {
-    name: 'bbb',
-    phone: '222',
-    email: 'bbb@b.com',
-  },
-  {
-    name: 'ccc',
-    phone: '333',
-    email: 'ccc@c.com',
-  },
-];
-
 function MemberModal(props) {
-  const {
-    visible,
-    onClose,
-    form,
-    currentItem,
-    loading,
-    depUserTree,
-    dataSource,
-    dispatch,
-  } = props;
+  const { visible, onClose, currentItem, loading, depUserTree, dataSource, dispatch } = props;
+  const [form] = Form.useForm();
   const [currentMember, setCurrentMember] = useState({});
 
   const columns = [
@@ -53,36 +24,14 @@ function MemberModal(props) {
     {
       title: '操作',
       //移除按钮,此处应传入当前成员的数据
-      render: member => <a onClick={() => onDelete(member)}>移除</a>,
+      render: member =>
+        member.ID != currentItem.author &&
+        member.ID != currentItem.LeaderId &&
+        member.ID != currentItem.wty_manager_id &&
+        member.ID != currentItem.opt_manager_id && <a onClick={() => onDelete(member)}>移除</a>,
     },
   ];
 
-  const renderUserSelectTreeNodes = data => {
-    return data.map(item => {
-      if (item.children) {
-        return (
-          <TreeNode
-            title={item.title}
-            key={item.key}
-            value={item.value}
-            dataRef={item}
-            selectable={item.selectable}
-          >
-            {renderUserSelectTreeNodes(item.children)}
-          </TreeNode>
-        );
-      }
-      return (
-        <TreeNode
-          title={item.title}
-          key={item.ID}
-          value={item.value}
-          selectable={item.selectable}
-        />
-      );
-    });
-  };
-
   const onDelete = item => {
     Modal.confirm({
       title: '移除成员',
@@ -100,17 +49,19 @@ function MemberModal(props) {
   };
 
   const handleAddMember = () => {
-    form.validateFields((error, { memberID }) => {
-      if (error) return;
+    form.validateFields().then(({ memberID }) => {
       dispatch({
         type: 'approval/addMember',
         payload: {
           project_code_id: currentItem.id,
-          user_id: memberID,
+          user_id: Number(memberID.split('||')[0]),
+        },
+        callback: () => {
+          form.resetFields();
         },
       });
     });
-    form.resetFields();
+    // form.resetFields();
   };
 
   return (
@@ -130,24 +81,25 @@ function MemberModal(props) {
         width="100%"
         style={{ marginBottom: 20 }}
         layout="inline"
+        form={form}
       >
-        <Form.Item label="添加成员">
-          {form.getFieldDecorator('memberID', {
-            initialValue: null,
-          })(
-            <TreeSelect
-              showSearch
-              allowClear
-              style={{ width: 240 }}
-              placeholder="请选择项目成员"
-              multiple={false}
-              filterTreeNode={(input, option) => {
-                return option.props.title === input;
-              }}
-            >
-              {renderUserSelectTreeNodes(depUserTree)}
-            </TreeSelect>
-          )}
+        <Form.Item
+          label="添加成员"
+          name="memberID"
+          initialValue={null}
+          rules={[{ required: true, message: '请选择成员' }]}
+        >
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: 240 }}
+            placeholder="请选择项目成员"
+            multiple={false}
+            treeData={depUserTree}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+          />
         </Form.Item>
         <Form.Item>
           <Button type="primary" loading={loading} onClick={handleAddMember}>
@@ -160,4 +112,4 @@ function MemberModal(props) {
   );
 }
 
-export default connect()(Form.create()(MemberModal));
+export default connect()(MemberModal);

+ 39 - 75
src/pages/PurchaseAdmin/PurchaseList/Approval/QualityOperateModal.js

@@ -1,7 +1,5 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Select, Modal, Input, TreeSelect } from 'antd';
+import { Form, Select, Modal, Input, TreeSelect } from 'antd';
 import { connect } from 'dva';
 const { Option } = Select;
 const { TreeNode } = TreeSelect;
@@ -11,59 +9,30 @@ function QualityOperateModal(props) {
     visible,
     onOk,
     onClose,
-    form,
     currentItem,
-    flowList = [],
     loading,
     depUserTree,
     dispatch,
     qualityOperate,
   } = props;
+  const [form] = Form.useForm();
 
-  //   const handleOk = () => {
-  //     form.validateFields((err, {managerID, contractStatus}) => {
-  //       if (err) return;
-  //       const [exe_manager_id,dep_id] = managerID.split("||")
-  //       dispatch({
-  //         type: 'approval/startExecution',
-  //         payload: {
-  //           project_code_id: currentItem.id,
-  //           with_contract: Number(contractStatus),
-  //           dep_id: Number(dep_id),
-  //           exe_manager_id: Number(exe_manager_id),
-  //         },
-  //         callback: () => onOk(),
-  //       })
-  //     });
-  //   };
-
-  const onChangeManager = (id, e) => {
-    console.log(id);
-  };
-
-  const renderUserSelectTreeNodes = data => {
-    return data.map(item => {
-      if (item.children) {
-        return (
-          <TreeNode
-            title={item.title}
-            key={item.key}
-            value={item.manage || item.value}
-            dataRef={item}
-            selectable={item.selectable}
-          >
-            {renderUserSelectTreeNodes(item.children)}
-          </TreeNode>
-        );
-      }
-      return (
-        <TreeNode
-          title={item.title}
-          key={item.ID}
-          value={item.manage || item.value}
-          selectable={item.selectable}
-        />
-      );
+  const handleOk = () => {
+    form.validateFields().then(({ managerID }) => {
+      const [manager_id, dep_id] = managerID.split('||');
+      let params = {};
+      params.type = qualityOperate ? 'approval/startOperate' : 'approval/startQuality';
+      params.payload = {
+        project_code_id: currentItem.id,
+        dep_id: Number(dep_id),
+      };
+      qualityOperate
+        ? (params.payload.opt_manager_id = Number(manager_id))
+        : (params.payload.wty_manager_id = Number(manager_id));
+      dispatch({
+        ...params,
+        callback: () => onOk(),
+      });
     });
   };
 
@@ -75,35 +44,30 @@ function QualityOperateModal(props) {
       destroyOnClose
       visible={visible}
       onCancel={onClose}
-      onOk={onOk}
+      onOk={handleOk}
     >
-      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
-        <Form.Item label={qualityOperate ? '运营经理' : '质保经理'}>
-          {form.getFieldDecorator('managerID', {
-            rules: [
-              {
-                required: true,
-                message: qualityOperate ? '请选择运营经理' : '请选择质保经理',
-              },
-            ],
-          })(
-            <TreeSelect
-              showSearch
-              allowClear
-              style={{ width: '100%' }}
-              placeholder={qualityOperate ? '请选择运营经理' : '请选择质保经理'}
-              multiple={false}
-              filterTreeNode={(input, option) => {
-                return option.props.title === input;
-              }}
-              onChange={onChangeManager}
-            >
-              {renderUserSelectTreeNodes(depUserTree)}
-            </TreeSelect>
-          )}
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label={qualityOperate ? '运营经理' : '质保经理'}
+          name="managerID"
+          rules={[
+            { required: true, message: qualityOperate ? '请选择运营经理' : '请选择质保经理' },
+          ]}
+        >
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: '100%' }}
+            placeholder={qualityOperate ? '请选择运营经理' : '请选择质保经理'}
+            multiple={false}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+            treeData={depUserTree}
+          ></TreeSelect>
         </Form.Item>
       </Form>
     </Modal>
   );
 }
-export default connect()(Form.create()(QualityOperateModal));
+export default connect()(QualityOperateModal);

+ 24 - 7
src/pages/PurchaseAdmin/PurchaseList/Approval/models/approval.js

@@ -13,6 +13,8 @@ import {
   addMember,
   queryMember,
   startExecution,
+  startOperate,
+  startQuality,
   deleteMember,
 } from '@/services/approval';
 import { message } from 'antd';
@@ -27,17 +29,17 @@ function getDepUserTree(data) {
 
   if (data.children) {
     data.children.forEach(item => {
-      getDepUserTree(item, false);
+      getDepUserTree(item);
     });
   }
 
   if (data.Users && data.Users.length !== 0) {
     data.Users.forEach(item => {
       item.title = item.CName;
-      item.key = item.ID;
-      item.value = item.ID;
-      item.manage =  item.ID + "||" + data.ID
+      item.key = item.ID + '||' + data.ID;
+      item.value = item.ID + '||' + data.ID;
       item.selectable = true;
+      item.DepId = data.ID;
       data.children.push(item);
     });
   }
@@ -130,7 +132,6 @@ export default {
       });
     },
     *queryMember({ payload }, { call, put }) {
-      console.log(111);
       const { data } = yield call(queryMember, payload);
       if (!data) return;
       yield put({
@@ -156,10 +157,8 @@ export default {
     },
     *createApproval({ payload, callback }, { call, put }) {
       const res = yield call(createApproval, payload);
-      console.log(res);
       if (res) {
         const { data } = res;
-        console.log(data);
         callback && callback();
         message.success('创建成功');
         yield put({
@@ -223,6 +222,24 @@ export default {
         type: 'queryApproval',
       });
     },
+    *startOperate({ payload, callback }, { call, put }) {
+      const res = yield call(startOperate, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('转运营送审成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+    *startQuality({ payload, callback }, { call, put }) {
+      const res = yield call(startQuality, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('转质保送审成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
   },
 
   reducers: {

+ 100 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/ApprovalProcess.tsx

@@ -0,0 +1,100 @@
+import React, { useState } from 'react';
+
+import { queryUserListByRoleID } from '@/services/boom';
+import { connect } from 'dva';
+import { PlusOutlined } from '@ant-design/icons';
+import { Popover, Radio, RadioChangeEvent, Spin, Steps } from 'antd';
+
+const { Step } = Steps;
+
+enum TYPR {
+  ROLE = 'role',
+  USER = 'user',
+}
+
+const ApprovalProcess = props => {
+  const { id, approvalProcess, userList, onChange } = props;
+  const [selectUserList, setSelectUserList] = useState([]);
+  const [curNodeIdx, setCurNodeIdx] = useState(-1);
+  const [loading, setLoading] = useState(false);
+  // console.log(approvalProcess);
+
+  const list = approvalProcess[id];
+  list?.forEach(item => {
+    if (item.length > 1 && item[0].type == TYPR.USER) {
+      item.forEach(curUser => {
+        curUser.name = userList.find(user => user.ID == curUser.value)?.CName || '-';
+      });
+    } else if (item.length == 1 && item[0].type == TYPR.USER) {
+      item[0].name = userList.find(user => user.ID == item[0].value)?.CName || '-';
+    } else if (item.length == 1 && item[0].nowType == TYPR.USER) {
+      item[0].name = userList.find(user => user.ID == item[0].nowValue)?.CName || '-';
+    } else {
+      item[0].name = null;
+    }
+  });
+  console.log(list);
+
+  const onStepsChange = async (current, list) => {
+    setLoading(true);
+    const itemNode = list[current][0];
+    if (itemNode.type !== 'role') return;
+    const data = await queryUserListByRoleID({ role_id: itemNode.value });
+    setCurNodeIdx(current);
+    setSelectUserList(data);
+    setLoading(false);
+  };
+
+  const selectedUserId = ({ target: { value } }: RadioChangeEvent) => {
+    //userId
+    const name = userList.find(user => user.ID == value)?.CName || '-';
+    const data = { nowType: TYPR.USER, nowValue: Number(value), name }; //type: TYPR.USER, value: Number(value)
+    list[curNodeIdx][0] = { ...list[curNodeIdx][0], ...data };
+    console.log(list);
+    onChange?.({ ...approvalProcess, [id]: list });
+  };
+
+  const content = (
+    <Spin spinning={loading}>
+      <Radio.Group onChange={selectedUserId}>
+        {selectUserList.map(item => (
+          // <Button onClick={() => selectedUserId(item.user_id)}>{item.c_name}</Button>
+          <Radio.Button value={item.user_id}>{item.c_name}</Radio.Button>
+        ))}
+      </Radio.Group>
+    </Spin>
+  );
+
+  return (
+    <>
+      <Steps current={-1} direction="vertical" onChange={value => onStepsChange(value, list)}>
+        {list?.map(item => (
+          <Step
+            key={item[0]?.value}
+            icon={
+              item[0].type != TYPR.USER || item[0].nowType == TYPR.USER ? (
+                <Popover
+                  placement="bottomLeft"
+                  title={'选择审批人'}
+                  content={content}
+                  trigger="click"
+                  overlayStyle={{ width: '300px' }}
+                >
+                  <PlusOutlined />
+                </Popover>
+              ) : null
+              // <PlusOutlined />
+            }
+            title={item[0]?.name}
+          />
+        ))}
+      </Steps>
+      {/* {selectUserList.map(item => (
+        <Button onClick={() => selectedUserId(item.user_id)}>{item.c_name}</Button>
+      ))} */}
+    </>
+  );
+};
+export default connect(({ xflow, detail, user }) => ({
+  userList: user.list,
+}))(ApprovalProcess);

+ 130 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/AuditDetailed.js

@@ -0,0 +1,130 @@
+import DDComponents from '@/components/DDComponents';
+import React, { useMemo, useState } from 'react';
+import { Form } from '@ant-design/compatible';
+import '@ant-design/compatible/assets/index.css';
+
+const layout = {
+  labelCol: {
+    span: 8,
+  },
+  wrapperCol: {
+    span: 16,
+  },
+};
+
+const AuditDetailed = props => {
+  // const [form] = Form.useForm();
+  const { items, form } = props;
+
+  const behavior = useMemo(() => {
+    let data = {};
+    items.forEach(d => {
+      const item = d.props;
+      if (item.behaviorLinkage) {
+        const key = item.id;
+        const options = item.options.map(o => {
+          let data;
+          try {
+            data = JSON.parse(o);
+          } catch (error) {
+            data = { key: o, value: o };
+          }
+          return data;
+        });
+        item.behaviorLinkage.forEach(b => {
+          const value = b.value;
+          b.targets.forEach(t => {
+            data[t.fieldId] = { key, value: options.find(o => o.key == value)?.value };
+          });
+        });
+      }
+    });
+
+    return data;
+  }, [items]);
+
+  const onFinish = values => {
+    console.log(values);
+  };
+
+  const GetComponent = item => {
+    const {
+      id,
+      label,
+      bizAlias,
+      required,
+      placeholder,
+      options,
+      align,
+      statField,
+      hideLabel,
+      objOptions,
+      format,
+      pushToAttendance,
+      labelEditableFreeze,
+      requiredEditableFreeze,
+      unit,
+      extract,
+      link,
+      payEnable,
+      bizType,
+      childFieldVisible,
+      notPrint,
+      verticalPrint,
+      hiddenInApprovalDetail,
+      disabled,
+      notUpper,
+      children, // 子控件
+    } = item.props;
+    // 判断是否属于关联项
+    if (behavior[id]) {
+      const { key, value } = behavior[id];
+      let currentValue = form.getFieldValue(key);
+      try {
+        currentValue = JSON.parse(currentValue);
+      } catch (error) {}
+      // 判断是否需要渲染
+      if (currentValue instanceof Array) {
+        if (currentValue?.indexOf(value) == -1) return null;
+      } else {
+        if (currentValue != value) return null;
+      }
+    }
+    let formLabel;
+    if (bizAlias) {
+      formLabel = bizAlias;
+    } else {
+      try {
+        // 判断label是否为JSON数组
+        formLabel = JSON.parse(label).join(',');
+      } catch (error) {
+        formLabel = label;
+      }
+    }
+
+    const component = DDComponents({ item });
+    if (!component) return null;
+    return (
+      <Form.Item label={formLabel}>
+        {typeof component == 'string'
+          ? component
+          : form.getFieldDecorator(id, {
+              rules: [{ required }],
+            })(component)}
+        {notUpper == 1 && <p>大写</p>}
+      </Form.Item>
+    );
+  };
+
+  return (
+    <Form
+      style={{ height: '400px', overflowY: 'scroll', paddingRight: 20 }}
+      layout="vertical"
+      autoComplete="off"
+    >
+      {items.map(item => GetComponent(item))}
+    </Form>
+  );
+};
+
+export default AuditDetailed;

+ 271 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/AuditFlow.js

@@ -0,0 +1,271 @@
+import React, { useMemo, useEffect, useState, useRef } from 'react';
+import { Steps, Popover, Alert, Button, Spin } from 'antd';
+import styles from './Index.less';
+import { queryDDProcessesForecast } from '@/services/boom';
+import { connect } from 'dva';
+
+const { Step } = Steps;
+
+function AuditFlow(props) {
+  const {
+    processCode,
+    deptId,
+    userId,
+    formComponentValues,
+    // activityId,
+    direction,
+    status,
+    tasks,
+    userList,
+    cc_userids = [],
+    canShowAudit = false,
+    onApprove,
+  } = props;
+  const [flow, setFlow] = useState({ workflowActivityRules: [] });
+  const [loading, setLoading] = useState(false);
+  const timerRef = useRef({
+    id: '',
+    status: false,
+  });
+  const current = useMemo(() => {
+    if (!tasks || tasks.length == 0) {
+      return false;
+    } else {
+      let current = 0;
+      flow.workflowActivityRules.forEach((item, index) => {
+        let task = tasks.find(task => task.activity_id == item.activityId);
+        if (task) {
+          if (task.task_status == 'COMPLETED') {
+            // 完成时节点为下一级
+            current = index + 1;
+          } else {
+            // 未完成则为当前节点
+            current = index;
+          }
+        }
+      });
+      return current;
+    }
+  }, [tasks, flow]);
+
+  const showAduit = useMemo(() => {
+    if (!canShowAudit) return false;
+    //当前处理中的审批人和登录人是一个人显示审批通过和拒绝
+    if (!tasks || tasks.length == 0) {
+      return false;
+    } else {
+      // 查询处于RUNNING的task activityId
+      const runningTask = tasks.filter(item => item.task_status == 'RUNNING');
+      // const task = tasks.find(task => task.activity_id == activityId);
+      if (runningTask.length == 0) return false;
+      // if (task.task_status == 'RUNNING' && task.userid == userId) return true;
+      return runningTask.find(task => task.userid == userId);
+    }
+  }, [tasks, userId]);
+
+  const getTaskId = () => {
+    const runningTask = tasks.find(item => item.task_status == 'RUNNING' && item.userid == userId);
+    if (runningTask) return runningTask.taskid;
+  };
+
+  const getDesc = item => {
+    const { activityId } = item;
+    if (!tasks || tasks.length == 0) return '';
+    if (item.activityName == '抄送人') {
+      let names = cc_userids
+        .map(userId => {
+          let user = userList.find(u => u.DingUserId == userId);
+          return user?.CName;
+        })
+        .filter(item => item);
+      return names.join(',');
+    } else {
+      const task = tasks.find(task => task.activity_id == activityId);
+      if (!task) return;
+      const user = userList.find(user => task.userid == user.DingUserId);
+      let result = null;
+      if (task.task_result !== 'NONE') {
+        result = (
+          <>
+            <br />
+            审批结果: {TASK_RESULT[task.task_result]}
+            <br />
+          </>
+        );
+      }
+      return (
+        <>
+          {user?.CName || '未知操作人'}({TASK_STATUS[task.task_status]}){result}
+        </>
+      );
+    }
+  };
+  const renderAlert = () => {
+    // const audit_comment = history.list[0]?.audit_comment;
+    let item = '';
+    switch (status) {
+      case -1:
+        item = <Alert message="已失效" type="error" />;
+        break;
+      case 0:
+        if (!flow.list || flow.list.FlowNodes?.length == 0) return;
+        item = <Alert message="审批拒绝" type="error" />;
+        break;
+      case 1:
+        item = <Alert message="等待审核中" type="info" />;
+        break;
+      case 2:
+        item = <Alert message={`审批被拒绝`} type="error" />;
+        break;
+      case 3:
+        item = <Alert message="审批通过" type="success" />;
+        break;
+      case -1:
+        item = <Alert message="已失效" type="error" />;
+        break;
+    }
+
+    return <div style={{ margin: '20px auto' }}>{item}</div>;
+  };
+  const customDot = (dot, { status, index }) => {
+    let item = flow.workflowActivityRules[index];
+
+    return (
+      <Popover
+        content={
+          <div>
+            节点类型:{ACTIVITY_TYPE[item.activityType]}
+            <br />
+            操作人类型:{ACTOR_TYPE[item.workflowActor?.actorType]}
+            <br />
+            审批类型:{APPROVAL_TYPE[item.workflowActor?.approvalType]}
+            <br />
+            审批方式:{APPROVAL_METHOD[item.workflowActor?.approvalMethod]}
+          </div>
+        }
+      >
+        {dot}
+      </Popover>
+    );
+  };
+
+  const getDetail = async () => {
+    if (!deptId || !userId) {
+      console.log('depId或userId不存在!');
+      return;
+    }
+    if (!timerRef.current.status) {
+      // 上锁
+      timerRef.current.status = true;
+      setLoading(true);
+      try {
+        let flow = await queryDDProcessesForecast({
+          processCode,
+          deptId,
+          userId,
+          formComponentValues,
+        });
+        setFlow(flow);
+        console.log(flow);
+      } catch (error) {}
+      setLoading(false);
+      setTimeout(() => {
+        // 延时解锁
+        timerRef.current.status = false;
+      }, 2000);
+    } else {
+      clearTimeout(timerRef.current.id);
+      // 延迟调用
+      timerRef.current.id = setTimeout(() => {
+        getDetail();
+      }, 2000);
+    }
+  };
+
+  useEffect(() => {
+    if (!processCode || !formComponentValues) return;
+    getDetail();
+  }, [processCode, formComponentValues]);
+
+  return (
+    <Spin spinning={loading}>
+      <div className={styles.top}>
+        <Steps current={current} progressDot={customDot} direction={direction}>
+          {flow.workflowActivityRules.map(item => (
+            <Step key={item.activityId} title={item?.activityName} description={getDesc(item)} />
+          ))}
+          {/* <Step key={item.activityId} title={item?.activityName} description={renderDesc(item)} /> */}
+        </Steps>
+        {showAduit && status != -1 && (
+          <div className={styles.btns} style={{ marginLeft: 80 }}>
+            <Button type="primary" onClick={() => onApprove(true, getTaskId())}>
+              审批通过
+            </Button>
+            <Button onClick={() => onApprove(false, getTaskId())} danger>
+              审批拒绝
+            </Button>
+          </div>
+        )}
+      </div>
+      {status !== undefined && renderAlert()}
+    </Spin>
+  );
+}
+
+const ACTOR_TYPE = {
+  approver: '审批人',
+  notifier: '抄送人',
+  audit: '办理人',
+};
+const APPROVAL_TYPE = {
+  MANUAL: '人工审批',
+  AUTO_AGREE: '自动通过',
+  AUTO_REFUSE: '自动拒绝',
+};
+const APPROVAL_METHOD = {
+  ONE_BY_ONE: '依次审批',
+  AND: '会签审批',
+  OR: '或签审批',
+};
+const ACTIVITY_TYPE = {
+  target_select: '自选审批人',
+  target_approval: '指定审批人',
+};
+const OPERATION_RESULT = {
+  AGREE: '同意',
+  REFUSE: '拒绝',
+  NONE: '未处理',
+};
+const OPERATION_TYPE = {
+  EXECUTE_TASK_NORMAL: '正常执行任务',
+  EXECUTE_TASK_AGENT: '代理人执行任务',
+  APPEND_TASK_BEFORE: '前加签任务',
+  APPEND_TASK_AFTER: '后加签任务',
+  REDIRECT_TASK: '转交任务',
+  START_PROCESS_INSTANCE: '发起流程实例',
+  TERMINATE_PROCESS_INSTANCE: '终止(撤销)流程实例',
+  FINISH_PROCESS_INSTANCE: '结束流程实例',
+  ADD_REMARK: '添加评论',
+  REDIRECT_PROCESS: '审批退回',
+  PROCESS_CC: '抄送',
+};
+
+const TASK_STATUS = {
+  NEW: '未启动',
+  RUNNING: '处理中',
+  PAUSED: '暂停',
+  CANCELED: '取消',
+  COMPLETED: '完成',
+  TERMINATED: '终止',
+};
+
+const TASK_RESULT = {
+  AGREE: '同意',
+  REFUSE: '拒绝',
+  REDIRECTED: '转交',
+  NONE: '未处理',
+};
+
+export default connect(({ user }) => ({
+  userList: user.list,
+}))(AuditFlow);

+ 502 - 72
src/pages/PurchaseAdmin/PurchaseList/Detail/CommitAuditModal.js

@@ -1,66 +1,116 @@
-import React, { useEffect, useState, useRef, useMemo } from 'react';
-import { Form } from '@ant-design/compatible';
+import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
 import '@ant-design/compatible/assets/index.css';
-import { Modal, Input, Select, message } from 'antd';
+import {
+  Modal,
+  Input,
+  Select,
+  message,
+  Cascader,
+  Form,
+  Tabs,
+  Row,
+  Col,
+  Empty,
+  Button,
+  Steps,
+  Popover,
+} from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
 import { connect } from 'dva';
+import { isArray, result, set } from 'lodash';
+import { useForm } from 'rc-field-form';
+import { async } from '@antv/x6/lib/registry/marker/async';
+import AuditDetailed from './AuditDetailed';
+import AuditFlow from './AuditFlow';
+import { queryDingSchema, queryProcessFlows } from '@/services/boom';
+import { Form as Form3x } from '@ant-design/compatible';
+import { getCurrentUser } from '@/utils/authority';
+import DDCode from '@/components/DDComponents/DDCode';
+import { uploadFile, queryUserListByRoleID } from '@/services/boom';
+import ApprovalProcess from './ApprovalProcess';
 
 const { TextArea } = Input;
 const { Option } = Select;
+const { TabPane } = Tabs;
+const { Step } = Steps;
 
 // 提交
 function CommitAuditModal(props) {
-  const { visible, onClose, onOk, form, loading, version, versionList, flowDetail } = props;
+  const {
+    dispatch,
+    visible,
+    onClose,
+    onOk,
+    loading,
+    version,
+    versionList,
+    flowDetail,
+    currentUser,
+    luckysheet,
+    userList,
+  } = props;
   const [auditId, setAuditId] = useState();
-  const handleOk = () => {
-    form.validateFields((err, fieldsValue) => {
-      if (err) return;
-      let approvalNode = flowDetail.nodes.find?.(item => item.Id == fieldsValue.node_id);
-      let serviceNode = flowDetail.nodes.find?.(
-        item => item.Id == fieldsValue.next_template_node_id
-      );
-      let params = {
-        desc: fieldsValue.desc,
-        // 审核流程id
-        flow_id: approvalNode?.flow_id || 0,
-        node_level_id: approvalNode?.flow_id ? 1 : 0,
-
-        id: version.id,
-        project_id: version.project_id,
-        cur_template_node_id: version.template_node_id * 1, // 当前节点
-        template_node_id: approvalNode?.Id, // 将要流转的节点
-        next_template_node_id: serviceNode.Id * 1, // 审核完成后的业务节点
-
-        // 模板id.一致就行
-        template_id: version.template_id,
-        cur_template_id: version.template_id,
-        next_template_id: version.template_id,
-      };
-      if (approvalNode?.Id) {
-        if (!approvalNode?.flow_id) {
-          message.error('审批节点未绑定审批流程!请联系管理员。');
-          return;
-        }
-      }
+  const [data, setData] = useState([]);
+  const [length, setLength] = useState(1);
+  const [formData, setFromData] = useState({});
+  const [auditList, setAuditList] = useState([]); //用于创建Tabs表单
+  const [formComponentValues, setFormComponentValues] = useState({}); //用于创建Tabs表单
+  const [form] = Form.useForm();
+  const [approvalProcess, setApprovalProcess] = useState({});
+  const [selectUserList, setSelectUserList] = useState([]);
+  const [curNodeIdx, setCurNodeIdx] = useState(-1);
 
-      // // 判断业务节点是否允许多清单
-      // if (!serviceNode.muti_version) {
-      //   //audit_status=4 表示为清单推进后留存的副本.不计入多清单计算
-      //   let serviceVersion = versionList.find(
-      //     item => item.audit_status != 4 && item.template_node_id == serviceNode.Id
-      //   );
-      //   if (serviceVersion) {
-      //     message.error(
-      //       `流转失败!业务节点【${serviceNode.label}】为单清单节点。已存在清单【${serviceVersion.version_name}】。`
-      //     );
-      //     return;
-      //   }
-      // }
-      setAuditId();
-      onOk(params);
-    });
+  useEffect(() => {
+    if (!visible) return;
+    const { edges, nodes } = flowDetail;
+    let Id = version.template_node_id;
+    const currentId = flowDetail.nodes.find?.(item => item.Id == Id)?.node_id;
+    const data = treeData(currentId);
+    if (data.length <= 0) setAuditId(currentId);
+    setData(data);
+  }, [auditId, version.template_node_id, visible]);
+
+  useEffect(() => {
+    form.resetFields();
+    setAuditList([]);
+  }, [visible]);
+
+  const treeData = currentId => {
+    const list = getNextNodes(currentId, 'custom-circle');
+    const fun = nodes => {
+      const re = nodes?.forEach((item, idx) => {
+        const data = getNextNodes(item.Id, 'custom-circle');
+        if (data || data.length > 0) list.push(...data);
+        fun(data);
+      });
+    };
+    fun(list);
+
+    const fun2 = list => {
+      const parents = list.filter(item => list.findIndex(node => node.Id == item.parentId) == -1);
+      let translator = (parents, children) => {
+        setLength(length + 1);
+        parents.forEach(parent => {
+          children.forEach((current, index) => {
+            if (current.parentId === parent.Id) {
+              let temp = JSON.parse(JSON.stringify(children));
+              temp.splice(index, 1);
+              translator([current], temp);
+              if (!parent.children.find(item => item.Id == current.Id))
+                parent.children.push(current);
+            }
+          });
+        });
+      };
+      translator(parents, list);
+      return parents;
+    };
+    return fun2(list);
   };
+
   const currentNodeId = useMemo(() => {
     let Id = version.template_node_id;
+    setAuditId(currentNodeId);
     return flowDetail.nodes.find?.(item => item.Id == Id)?.node_id;
   }, [flowDetail, version]);
   /**
@@ -72,16 +122,34 @@ function CommitAuditModal(props) {
   const getNextNodes = (currentId, type) => {
     const { edges, nodes } = flowDetail;
     if (!currentId) return [];
+    //删除虚线通向的节点
+    // let targetIds = edges
+    //   .filter(edge => {
+    //     let line = edge.attrs?.line?.strokeDasharray?.split(' ');
+    //     return edge.source.cell == currentId && line && line[0] == '0';
+    //   })
+    //   .map(item => item.target.cell);
     let targetIds = edges
       .filter(edge => edge.source.cell == currentId)
       .map(item => item.target.cell);
+
+    edges.filter(edge => edge.source.cell == currentId);
     let auditNodes = nodes.filter(node => {
       if (type && node.name != type) {
         return false;
       }
       return targetIds.indexOf(node.id) != -1;
     });
-    return auditNodes || [];
+    const result = auditNodes.map(item => {
+      return {
+        label: item.label,
+        value: item.Id,
+        Id: item.node_id,
+        parentId: currentId,
+        children: [],
+      };
+    });
+    return result || [];
   };
 
   const changeAudit = id => {
@@ -89,44 +157,406 @@ function CommitAuditModal(props) {
     setAuditId(node.node_id);
   };
 
+  const onChange = value => {
+    changeAudit(value[value.length - 1]);
+    setAuditListFun();
+  };
+
+  const getReComputeAudit = (items, changedValues) => {
+    const id = Object.keys(changedValues)[0];
+    const formItem = items?.find(item => item.props.id == id);
+    if (formItem && formItem.props?.required) return true;
+    return false;
+  };
+
+  //填写表单实时计算审批流程
+  const advanceSubmit = async () => {
+    console.log('重重新计算审批流程');
+    var fieldsValue = await form.validateFields();
+    let hasFlowId = true; //是否都绑定审批节点
+
+    let result = Object.values(fieldsValue)
+      .map(item => {
+        if (item && Array.isArray(item)) return item;
+      })
+      .filter(item => item);
+    const formList = await getFromData(result);
+    let params = {
+      desc: fieldsValue.desc,
+      // 审核流程id
+      flow_id: 0,
+      node_level_id: 0,
+      id: version.id,
+      project_id: version.project_id,
+      cur_template_node_id: version.template_node_id * 1, // 当前节点
+      next_template_node_id: 0, // 审核完成后的业务节点
+      template_node_id: null, // 将要流转的节点审批节点
+      flow_path: null, //审批节点数组
+      // 模板id.一致就行
+      template_id: version.template_id,
+      cur_template_id: version.template_id,
+      next_template_id: version.template_id,
+      form_list: formList,
+    };
+
+    const ddd = {
+      '778': [
+        { type: 'user', value: 448, origin: 0 },
+        { type: 'role', value: 82, origin: 0 },
+        { type: 'role', value: 61, origin: 0 },
+      ],
+      '791': [
+        { type: 'user', value: 448, origin: 0 },
+        { type: 'role', value: 82, origin: 0 },
+        { type: 'role', value: 61, origin: 0 },
+      ],
+      '792': [
+        { type: 'user', value: 448, origin: 0 },
+        { type: 'role', value: 82, origin: 0 },
+        { type: 'role', value: 29, origin: 0 },
+      ],
+    };
+
+    dispatch({
+      type: 'detail/advanceSubmitNextNode',
+      payload: params, //values,
+      callback: data => {
+        if (data) {
+          setApprovalProcess(data);
+        }
+        console.log('=======================================', data);
+      },
+    });
+  };
+
+  //处理tabs页
+  const setAuditListFun = async () => {
+    var fieldsValue = await form.validateFields();
+    let addAuditList = [];
+    let result = Object.values(fieldsValue)
+      .map(item => {
+        if (item && Array.isArray(item)) return item;
+      })
+      .filter(item => item)
+      .flat(Infinity);
+    let nodeList = [...new Set(result)]
+      .map(Id => {
+        return flowDetail.nodes.find?.(item => item.Id == Id);
+      })
+      .filter(item => item);
+    let flowIds = [...new Set(nodeList.map(item => item.flow_id))].join(',');
+    let data = await queryProcessFlows({ ids: flowIds });
+    if (data && data?.length > 0) {
+      let newlist = nodeList.map(node => {
+        let curData = data.find(item => item.id == node.flow_id);
+        let newItem = {
+          name: curData?.name,
+          nodeId: node.Id,
+          items: JSON.parse(curData.form_json),
+        };
+        return newItem;
+      });
+      addAuditList = [...addAuditList, ...newlist];
+    }
+    addAuditList.forEach((item, index) => {
+      let Components = Form3x.create({
+        onValuesChange: (props, changedValues, allValues) => {
+          const { items } = props;
+          formComponentValues[item.nodeId] = items
+            .map(item => {
+              const itemProps = item.props;
+              let val = allValues[itemProps.id];
+              if (!itemProps.label || val === '') return;
+              if (val instanceof Object) {
+                return {
+                  name: itemProps.label,
+                  id: itemProps.id,
+                  value: [...val],
+                };
+              } else if (allValues[itemProps.id]) {
+                return {
+                  name: itemProps.label,
+                  id: itemProps.id,
+                  value: [allValues[itemProps.id]] || undefined,
+                };
+              }
+            })
+            .filter(item => item);
+          if (getReComputeAudit(items, changedValues)) advanceSubmit();
+          console.log('==================================', { ...formComponentValues });
+          setFormComponentValues({ ...formComponentValues });
+        },
+      })(AuditDetailed);
+      item.FormComponents = <Components items={item.items} />;
+    });
+    setAuditList(addAuditList);
+  };
+
+  const getFromData = async idList => {
+    const data = formComponentValues;
+    const result = [];
+    //获取流转节点的层级关系
+    let len = 0;
+    let list = [];
+    idList.forEach(item => {
+      if (len < item.length) len = item.length;
+    });
+    for (let i = 0; i < len; i++) {
+      idList.forEach(item => {
+        if (item && item[i]) list.push(item[i]);
+      });
+    }
+    let firstList = [...new Set(list)];
+    // let attachment = await upload();
+    firstList.forEach(id => {
+      let approvalNode = flowDetail.nodes.find?.(item => item.Id == id);
+      let values = data[approvalNode.Id] || [];
+      const audit_list = approvalProcess[approvalNode.Id]?.map(item => {
+        if (item[0].type == 'role') return item[0].nowValue;
+        return item[0].value;
+      });
+      const formItem = {
+        flow_id: approvalNode.flow_id,
+        template_node_id: approvalNode.Id,
+        formComponentValues: [...values], //{ name: '附件', value: JSON.stringify(attachment) }
+        audit_list: audit_list || [],
+      };
+      result.push(JSON.stringify(formItem));
+    });
+    return result;
+  };
+
+  const getFlowPath = node => {
+    //[134, 135]
+    let itemData = {};
+    const Function = (curId, index) => {
+      if (!curId) return;
+      let data = {};
+      let approvalNode = flowDetail.nodes.find?.(item => item.Id == curId);
+      data.template_id = version.template_id;
+      data.flow_id = approvalNode?.flow_id || 0;
+      data.node_level_id = approvalNode?.flow_id ? 1 : 0;
+      data.template_node_id = approvalNode?.Id;
+      index++;
+      if (approvalNode?.Id) {
+        if (!approvalNode?.flow_id) {
+          hasFlowId = false;
+        }
+      }
+      const res = Function(node[index], index);
+      if (res) {
+        data.flow_path = [res];
+      }
+      return data;
+    };
+    itemData = Function(node[0], 0);
+    return itemData;
+  };
+
+  const onFinish = async () => {
+    console.log(approvalProcess);
+    const isOk = Object.values(approvalProcess).every(item => {
+      console.log(item);
+      return item.every(cur => {
+        if (cur[0].type == 'role') return cur[0].nowValue;
+        return true;
+      });
+    });
+    if (!isOk) {
+      message.error('请选择审批人。');
+      return;
+    }
+    var fieldsValue = await form.validateFields();
+    let hasFlowId = true; //是否都绑定审批节点
+    const getFlowPath = node => {
+      //[134, 135]
+      let itemData = {};
+      const Function = (curId, index) => {
+        if (!curId) return;
+        let data = {};
+        let approvalNode = flowDetail.nodes.find?.(item => item.Id == curId);
+        data.template_id = version.template_id;
+        data.flow_id = approvalNode?.flow_id || 0;
+        data.node_level_id = approvalNode?.flow_id ? 1 : 0;
+        data.template_node_id = approvalNode?.Id;
+        index++;
+        if (approvalNode?.Id) {
+          if (!approvalNode?.flow_id) {
+            hasFlowId = false;
+          }
+        }
+        const res = Function(node[index], index);
+        if (res) {
+          data.flow_path = [res];
+        }
+        return data;
+      };
+      itemData = Function(node[0], 0);
+      return itemData;
+    };
+    let result = Object.values(fieldsValue)
+      .map(item => {
+        if (item && Array.isArray(item)) return item;
+      })
+      .filter(item => item);
+    let serviceNode = flowDetail.nodes.find?.(item => item.Id == fieldsValue.next_template_node_id);
+
+    if (!serviceNode) {
+      message.error('请选择需要流转的业务节点。');
+      return;
+    }
+
+    const flowPath = result.map(item => getFlowPath(item));
+    const formList = await getFromData(result);
+    let params = {
+      desc: fieldsValue.desc,
+      // 审核流程id
+      // flow_id: approvalNode?.flow_id || 0,
+      // node_level_id: approvalNode?.flow_id ? 1 : 0,
+
+      id: version.id,
+      project_id: version.project_id,
+      cur_template_node_id: version.template_node_id * 1, // 当前节点
+      next_template_node_id: serviceNode.Id * 1, // 审核完成后的业务节点
+      // template_node_id: result[0][0], // 将要流转的节点审批节点
+      // flow_path:flow_path, //审批节点数组
+      // 模板id.一致就行
+      template_id: version.template_id,
+      cur_template_id: version.template_id,
+      next_template_id: version.template_id,
+    };
+    if (result.length <= 0) {
+      //直接走业务节点
+    } else if (result.length <= 1 && result[0]?.length <= 1) {
+      //单个审批节点
+      let approvalNode = flowDetail.nodes.find?.(item => item.Id == result[0][0]);
+      params.flow_id = approvalNode?.flow_id || 0;
+      params.node_level_id = approvalNode?.flow_id ? 1 : 0;
+      params.template_node_id = result[0][0]; // 将要流转的节点审批节点
+      params.form_list = formList; //创建钉钉表单所需数据
+      if (approvalNode?.Id) {
+        if (!approvalNode?.flow_id) {
+          hasFlowId = false;
+        }
+      }
+    } else {
+      //多节点审批
+      params.template_node_id = result[0][0]; // 将要流转的节点审批节点
+      params.flow_path = flowPath;
+      params.form_list = formList; //创建钉钉表单所需数据
+    }
+    if (!hasFlowId) {
+      message.error('当前存在审批节点未绑定审批流程!请联系管理员。');
+      return;
+    }
+    onOk(params);
+  };
+  const CascaderNode = index => {
+    return (
+      <Form.Item
+        labelCol={{ span: 7 }}
+        wrapperCol={{ span: 15 }}
+        label={`审批节点${index + 1}`}
+        name={`circle${index}`}
+        key={`circle${index}`}
+      >
+        <Cascader style={{ width: '100%' }} options={data} onChange={onChange} />
+      </Form.Item>
+    );
+  };
+  const upload = async () => {
+    let blob = await luckysheet.current.getExcelBolb();
+    let formData = new FormData();
+    formData.append('userid', currentUser.DingUserId);
+    formData.append('file', new File([blob], `${version.version_name}_${version.version_no}.xlsx`));
+
+    try {
+      let res = await uploadFile(formData);
+      let data = JSON.parse(res.dentry);
+      return [
+        {
+          spaceId: String(data.spaceId),
+          fileName: data.name,
+          fileSize: String(data.spaceId),
+          fileType: data.extension,
+          fileId: data.id,
+        },
+      ];
+    } catch (error) {
+      message.error('附件上传失败');
+    }
+  };
+
   return (
     <Modal
       confirmLoading={loading}
       destroyOnClose
       title="提交流转目标"
+      width={1000}
       visible={visible}
       onCancel={() => {
         setAuditId();
         onClose();
       }}
-      onOk={handleOk}
+      onOk={onFinish}
     >
-      <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label="审批节点">
-        {form.getFieldDecorator('node_id')(
-          <Select style={{ width: '100%' }} onChange={changeAudit}>
-            {getNextNodes(currentNodeId, 'custom-circle').map(item => (
-              <Option key={item.Id}>{item.label}</Option>
-            ))}
-          </Select>
-        )}
-      </Form.Item>
-      <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label="业务节点">
-        {form.getFieldDecorator('next_template_node_id')(
+      <Form form={form}>
+        {data.map((item, idx) => (data.length == 1 ? CascaderNode('') : CascaderNode(idx)))}
+        <Form.Item
+          labelCol={{ span: 7 }}
+          wrapperCol={{ span: 15 }}
+          label="业务节点"
+          name="next_template_node_id"
+        >
           <Select style={{ width: '100%' }}>
-            {getNextNodes(auditId || currentNodeId, auditId ? 'custom-rect' : '').map(item => (
-              <Option key={item.Id}>{item.label}</Option>
+            {getNextNodes(data.length < 0 ? currentNodeId : auditId, 'custom-rect').map(item => (
+              <Option key={item.value}>{item.label}</Option>
             ))}
           </Select>
-        )}
-      </Form.Item>
-      <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label="备注信息">
-        {form.getFieldDecorator('desc')(<Input.TextArea />)}
-      </Form.Item>
+        </Form.Item>
+        <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label="备注信息" name="desc">
+          <Input.TextArea />
+        </Form.Item>
+        {/* <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label="授权码" name="ddCode">
+          <DDCode />
+          <Button onClick={upload}>上传文件</Button>
+        </Form.Item> */}
+      </Form>
+      <Tabs defaultActiveKey="1">
+        {auditList.map((item, idx) => (
+          <TabPane tab={item.name} key={`${idx}_${item.title}`}>
+            <Row>
+              <Col span={17}>{item.FormComponents}</Col>
+              <Col offset={1} span={4}>
+                {!formComponentValues[item.nodeId] || !approvalProcess[item.nodeId] ? (
+                  <Empty description="请先填写表单" />
+                ) : (
+                  <ApprovalProcess
+                    id={item.nodeId}
+                    approvalProcess={approvalProcess}
+                    onChange={setApprovalProcess}
+                  />
+                )
+                // <AuditFlow
+                //   processCode={item.formCode}
+                //   formComponentValues={formComponentValues[item.nodeId]}
+                //   direction={'vertical'}
+                //   deptId={'14237557'}
+                //   userId={currentUser.DingUserId || getCurrentUser()?.DingUserId}
+                // />
+                }
+              </Col>
+            </Row>
+          </TabPane>
+        ))}
+      </Tabs>
     </Modal>
   );
 }
 
-export default connect(({ xflow, detail }) => ({
+export default connect(({ xflow, detail, user }) => ({
   flowDetail: xflow.flowDetail,
   versionList: detail.versionList,
-}))(Form.create()(CommitAuditModal));
+  currentUser: user.currentUser,
+  userList: user.list,
+}))(CommitAuditModal);

+ 24 - 26
src/pages/PurchaseAdmin/PurchaseList/Detail/ExportModal.js

@@ -34,34 +34,34 @@ function CompareModal(props) {
   const [checkValue, setCheckValue] = useState([]);
   const [tabList, setTabList] = useState([]);
   const [active, setActive] = useState();
-  
-  useEffect(() => {
-      const list = []
-      if (sheet && sheet.length > 0) {
-        const sheetData = JSON.parse(JSON.stringify(sheet));
-        sheetData.forEach(item => {
-            let obj = {}
-            obj.name = item.name;
-            if(item.data && item.data[0]){
-                obj.list = item.data[0]?.filter( cur => cur);
-            }
-            obj.id = item.index;
-            list.push(obj)
-          });
 
-          setCheckValue([]);
-          setTabList(list);
-          setActive(list[0]?.id);
+  useEffect(() => {
+    const list = [];
+    if (sheet && sheet.length > 0) {
+      const sheetData = JSON.parse(JSON.stringify(sheet));
+      sheetData.forEach(item => {
+        let obj = {};
+        obj.name = item.name;
+        if (item.data && item.data[0]) {
+          obj.list = item.data[0]?.filter(cur => cur);
         }
-      },[sheet])
+        obj.id = item.index;
+        list.push(obj);
+      });
+
+      setCheckValue([]);
+      setTabList(list);
+      setActive(list[0]?.id);
+    }
+  }, [sheet]);
 
   const onChange = check => {
-      setCheckValue(check);
-      console.log(check)
+    setCheckValue(check);
+    console.log(check);
   };
 
   const handleOk = () => {
-    onOk(checkValue)
+    onOk(checkValue);
   };
 
   return (
@@ -77,13 +77,11 @@ function CompareModal(props) {
           {tabList.map(tab => (
             <TabPane tab={tab.name} key={tab.id}>
               <Row>
-                {tab.list.map(item => 
-                (
+                {tab.list.map(item => (
                   <Col span={8} key={item.m}>
                     <Checkbox value={item.v}>{item.v}</Checkbox>
                   </Col>
-                )
-                )}
+                ))}
               </Row>
             </TabPane>
           ))}
@@ -92,4 +90,4 @@ function CompareModal(props) {
     </Modal>
   );
 }
-export default CompareModal
+export default CompareModal;

+ 82 - 65
src/pages/PurchaseAdmin/PurchaseList/Detail/FilesModal.js

@@ -9,70 +9,87 @@ import moment from 'moment/moment';
 import PreviewFile from '@/components/PreviewFile';
 // 历史清单
 function FilesModal(props) {
-    const { visible, onClose, onUpload, data, uploadProps, DeleteFile, downloadFile, loading } = props;
-    const handleSelect = item => {
-        // onSelect(item);
-        // onClose();
-    };
-    const columns = [
-        {
-            title: '预览',
-            dataIndex: 'name',
-            render: (text, item) => {
-                return <PreviewFile name={item.name} src={item.url} />;
-            },
-        },
-        {
-            title: '上传时间',
-            dataIndex: 'c_time',
-            render: text => {
-                return text ? moment(text).format('YYYY年MM月DD日  HH:mm:ss') : null;
-            },
-        },
-        {
-            title: '上传人',
-            dataIndex: 'CreatorUser',
-            render: record => (record.CName || '')
-        },
-        {
-            title: '操作',
-            render: record => (
-                <>
-                    <a onClick={() => { downloadFile(record) }}>
-                        查看
-                    </a>
-                    <a onClick={() => {
-                        confirm({
-                            title: '提醒',
-                            content: '确认删除该文件,删除后无法复原',
-                            okText: '确认',
-                            cancelText: '取消',
-                            onOk() {
-                                DeleteFile(record.id);
-                            },
-                        });
+  const {
+    visible,
+    onClose,
+    onUpload,
+    data,
+    uploadProps,
+    DeleteFile,
+    downloadFile,
+    loading,
+  } = props;
+  const handleSelect = item => {
+    // onSelect(item);
+    // onClose();
+  };
+  const columns = [
+    {
+      title: '预览',
+      dataIndex: 'name',
+      render: (text, item) => {
+        return <PreviewFile name={item.name} src={item.url} />;
+      },
+    },
+    {
+      title: '上传时间',
+      dataIndex: 'c_time',
+      render: text => {
+        return text ? moment(text).format('YYYY年MM月DD日  HH:mm:ss') : null;
+      },
+    },
+    {
+      title: '上传人',
+      dataIndex: 'CreatorUser',
+      render: record => record.CName || '',
+    },
+    {
+      title: '操作',
+      render: record => (
+        <>
+          <a
+            onClick={() => {
+              downloadFile(record);
+            }}
+          >
+            查看
+          </a>
+          <a
+            onClick={() => {
+              confirm({
+                title: '提醒',
+                content: '确认删除该文件,删除后无法复原',
+                okText: '确认',
+                cancelText: '取消',
+                onOk() {
+                  DeleteFile(record.id);
+                },
+              });
+            }}
+            style={{ marginLeft: 10 }}
+          >
+            删除
+          </a>
+        </>
+      ),
+    },
+  ];
 
-                    }} style={{ marginLeft: 10 }}>删除</a>
-                </>
-            ),
-        },
-    ];
-
-    const onClick = item => {
-        // onClose();
-        // onSelect(item);
-    };
-    return (
-        <Modal title="附件列表" width="60%" onCancel={onClose} visible={visible} footer={false}>
-            <div>
-                <Upload {...uploadProps}>
-                    <Button type="primary" style={{ marginBottom: 20 }} loading={loading}>
-                        <UploadOutlined /> 上传文件
-                    </Button>
-                </Upload>
-            </div>
-            <Table rowKey="id" columns={columns} dataSource={data} loading={loading} />
-        </Modal>
-    );
+  const onClick = item => {
+    // onClose();
+    // onSelect(item);
+  };
+  return (
+    <Modal title="附件列表" width="60%" onCancel={onClose} visible={visible} footer={false}>
+      <div>
+        <Upload {...uploadProps}>
+          <Button type="primary" style={{ marginBottom: 20 }} loading={loading}>
+            <UploadOutlined /> 上传文件
+          </Button>
+        </Upload>
+      </div>
+      <Table rowKey="id" columns={columns} dataSource={data} loading={loading} />
+    </Modal>
+  );
 }
-export default FilesModal
+export default FilesModal;

+ 284 - 62
src/pages/PurchaseAdmin/PurchaseList/Detail/FlowModal.js

@@ -1,26 +1,46 @@
-import React, { useEffect, useState, useRef, useMemo } from 'react';
-import { Modal, Input, Select, List, Row, Col } from 'antd';
+import React, { useEffect, useState, useRef, useMemo, memo } from 'react';
+import { Modal, Input, Select, List, Row, Col, Table, message, Steps, Space, Button } from 'antd';
 import Flow from '@/components/Flow/index';
 import { connect } from 'dva';
 import { GetTokenFromUrl, getToken } from '@/utils/utils';
 import { MODELS, useXFlowApp, useModelAsync } from '@antv/xflow';
 import { CheckOutlined } from '@ant-design/icons';
-import { queryVserionByNode } from '@/services/boom';
+import { queryDingInstanceDetail, queryRecordSheet, queryVserionByNode } from '@/services/boom';
 import { async } from '@antv/x6/lib/registry/marker/async';
+import VersionModal from './VersionModal';
+import AuditFlow from './AuditFlow';
+
+const { Step } = Steps;
 
 const { TextArea } = Input;
 const localData = JSON.parse(localStorage.ggDetaiData || '{}');
+const PAGE_SIZE = 8;
 
 // 提交
 function FlowModal(props) {
-  const { visible, version, onClose, onChangeVersion, form, loading, flowDetail } = props;
+  const {
+    visible,
+    version,
+    onClose,
+    onChangeVersion,
+    form,
+    loading,
+    flowDetail,
+    dispatch,
+    isOut,
+    onCommit,
+    currentUser,
+  } = props;
   const [data, setData] = useState([]);
   const [nodeLoading, setNodeLoading] = useState(false);
-  // const app = useXFlowApp();
-  // const appRef = userRef()
+  const [pageSize, setPageSize] = useState(PAGE_SIZE);
+  const [stepsData, setStepsData] = useState([]);
+  const [versionVisible, setVersionVisible] = useState(false);
+  let token = getToken();
 
   const graphData = useMemo(() => {
-    let nodes = flowDetail.nodes.map(item => ({
+    if (!flowDetail) return;
+    let nodes = flowDetail.nodes?.map(item => ({
       ...item,
       isCheck: item.Id == version.template_node_id,
     }));
@@ -30,81 +50,281 @@ function FlowModal(props) {
     };
   }, [flowDetail, version.template_node_id]);
 
+  useEffect(() => {
+    if (!visible) updateSteps([]);
+  }, [visible, version]);
+
+  useEffect(() => {
+    if (stepsData.length <= 0) {
+      setPageSize(PAGE_SIZE);
+    } else {
+      setPageSize(PAGE_SIZE - stepsData.length);
+    }
+  }, [stepsData]);
+
   const handleSelectNode = async args => {
     let res;
     const id = args.nodeId || args.nodeIds[0];
     if (!id) return;
     let node = graphData.nodes.find(item => item.id == id);
-    // setData(node.version);
     setNodeLoading(true);
     try {
       res = await queryVserionByNode({ template_node_id: node.Id });
       let data = [];
-      Object.values(res.data.excel_version).map(arr => {
+      if (!res.data.excel_version_tree) setData([]);
+      res.data.excel_version_tree?.map(arr => {
         if (res.data.flow_id) {
-          data = [...data, arr[arr.length - 1]];
+          data = [...data, { ...arr, flow_id: res.data.flow_id }];
         } else {
-          data = [...data, ...arr];
+          data = [...data, arr];
         }
       });
+      data.sort((a, b) => b.id - a.id);
+      data.forEach((item, id) => {
+        //解决key报错问题
+        data[id].key = `${id}-${item.name}`;
+        item.isParent = true;
+      });
+      console.log(data);
       setData(data);
     } catch (error) {
       console.log(error);
     }
     setNodeLoading(false);
+    updateSteps([]);
+  };
+
+  const updateSteps = (data, curNodeId) => {
+    let newData = [];
+    let set = new Set();
+    data.forEach(item => set.add(item.template_node_id));
+    let list = [...set];
+    if (set.has(curNodeId)) {
+      set.delete(curNodeId);
+      list = [curNodeId, ...set];
+    }
+    let dataList = list.map(template_node_id => {
+      let itemDataList = data.filter(item => item.template_node_id == template_node_id);
+      let curid = 3;
+      let status = 'process';
+      itemDataList.forEach(item => {
+        if (item.audit_status != 3 && item.node_id <= curid) curid = item.node_id - 1;
+        if (item.audit_status == 2) status = 'error';
+      });
+      let curNode = flowDetail.nodes.find(item => item.Id == itemDataList[0].template_node_id);
+      const seqList = itemDataList[0].FlowInfo.FlowNodes.filter(
+        item => item.template_node_id == template_node_id
+      ).sort((a, b) => a.seq - b.seq);
+      let obj = {
+        status,
+        current: curid,
+        list: seqList,
+        name: curNode?.label || itemDataList[0].FlowInfo.name,
+      };
+      itemDataList.forEach((itemData, idx) => {
+        if (idx >= obj.list.length) return;
+        obj.list[idx].auditor = itemData.AuthorInfo.CName;
+      });
+      return obj;
+    });
+    console.log(dataList);
+    setStepsData(dataList);
+  };
+
+  // const updateSteps = async (data, curNodeId) => {
+  //   const dataList = [];
+  //   for (let i = 0; i < data.length; i++) {
+  //     let curNode = flowDetail.nodes.find(item => item.Id == data[i].template_node_id);
+  //     console.log(curNode);
+  //     const response = await queryDingInstanceDetail({
+  //       process_instance_id: data[i].ding_instance_id, //创建表单成功返回的id
+  //     });
+  //     if (response) {
+  //       const processInstance = response.data?.process_instance;
+  //       let data = {
+  //         processCode: '',
+  //         deptId: '14169890',
+  //         tasks: [],
+  //         // userId: '16569001414345099',
+  //         // deptId: currentUser.DingDepId || getCurrentUser()?.DingDepId,
+  //         userId: currentUser.DingUserId || getCurrentUser()?.DingUserId,
+  //         formComponentValues: [],
+  //         activityId: '',
+  //         cc_userids: [],
+  //         status: 'undefined',
+  //         name: curNode?.label || '未知节点',
+  //       };
+  //       if (processInstance?.tasks && processInstance.tasks?.length > 0) {
+  //         // let item = flowDetail.nodes.find(item => item.Id == version.template_node_id);
+  //         // if (!item) return data;
+  //         const { tasks, form_component_values, cc_userids } = processInstance;
+  //         data.processCode = curNode.process_code;
+  //         data.activityId = tasks[tasks.length - 1]?.activity_id;
+  //         data.tasks = tasks || [];
+  //         data.cc_userids = cc_userids;
+  //         data.formComponentValues = form_component_values?.filter(curNode => curNode.name);
+  //       }
+  //       dataList.push(data);
+  //     }
+  //   }
+  //   console.log(dataList);
+  //   setStepsData(dataList);
+  // };
+
+  const handleChangeClick = item => {
+    let file = isOut ? 'newList' : 'detail';
+    let type = item.flow_id ? '/queryAuditRecord' : '/queryAuditExcel';
+    console.log(`${file}${type}`, item);
+    dispatch({
+      type: `${file}${type}`,
+      payload: {
+        excel_id: item.id,
+        pageSize: 100,
+      },
+      callback: res => {
+        updateSteps(res, item.template_node_id);
+      },
+    });
+  };
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: '名称',
+        // width: '33%',
+        render: item => (
+          <span style={{ color: item.audit_status != 0 ? '#9b9b9b' : '' }}>
+            {item.id == version.id && !item.isParent && (
+              <CheckOutlined style={{ marginRight: 10 }} />
+            )}
+            {item.version_no && !item.isParent
+              ? `${item.version_name}.${item.version_no}`
+              : item.version_name}
+          </span>
+        ),
+      },
+      {
+        title: '状态',
+        width: '30%',
+        render: item => {
+          if (!item.flow_id && item.isParent) return;
+          let style = { color: getColor(item) };
+          let txt = '';
+          switch (item.audit_status) {
+            case 0:
+              txt = '未提交';
+              break;
+            case 1:
+              txt = '待审批';
+              break;
+            case 2:
+              txt = '已拒绝';
+              break;
+            case 3:
+              txt = '已通过';
+              break;
+            case 4:
+              txt = '已提交';
+              break;
+          }
+          if (item.status == 1) txt = '已失效';
+          return item.audit_status != 0 ? (
+            <Button onClick={() => handleChangeClick(item)}>{txt}</Button>
+          ) : (
+            <span style={style}>{txt}</span>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: '20%',
+        render: item =>
+          (item.flow_id || !item.isParent) &&
+          item.id != version.id && (
+            <a
+              onClick={() => {
+                console.log(item);
+                onChangeVersion(item);
+                onClose();
+              }}
+            >
+              加载
+            </a>
+          ),
+      },
+    ];
+  }, [version]);
+
+  const onChange = () => {
+    updateSteps([]);
   };
 
   return (
-    <Modal
-      confirmLoading={loading}
-      destroyOnClose
-      title="流程图"
-      visible={visible}
-      onCancel={onClose}
-      footer={false}
-      width="98%"
-    >
-      <Row gutter={8}>
-        <Col span={18}>
-          <Flow meta={{ type: 'view' }} flowDetail={graphData} onSelectNode={handleSelectNode} />
-        </Col>
-        <Col span={6}>
-          <List
-            size="small"
-            header={<div>清单列表</div>}
-            bordered
-            dataSource={data}
-            loading={nodeLoading}
-            renderItem={item => {
-              const isCurrent = item.id == version.id;
-              return (
-                <List.Item
-                  actions={
-                    isCurrent
-                      ? null
-                      : [
-                          <a
-                            onClick={() => {
-                              onChangeVersion(item);
-                              onClose();
-                            }}
-                          >
-                            切换
-                          </a>,
-                        ]
-                  }
+    <>
+      <Modal
+        confirmLoading={loading}
+        destroyOnClose
+        title="流程图"
+        visible={visible}
+        onCancel={onClose}
+        footer={false}
+        width="98%"
+      >
+        <Row gutter={8}>
+          <Col span={16}>
+            <Flow meta={{ type: 'view' }} flowDetail={graphData} onSelectNode={handleSelectNode} />
+          </Col>
+          <Col span={8}>
+            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+              <div style={{ fontSize: '16px' }}>清单列表</div>
+              {isOut && (
+                <Button
+                  type="primary"
+                  style={{ float: 'right', marginBottom: '10px' }}
+                  onClick={() => setVersionVisible(true)}
                 >
-                  <div style={{ color: getColor(item) }}>
-                    {isCurrent && <CheckOutlined style={{marginRight: 10}}/>}
-                    {item.version_name}
-                  </div>
-                </List.Item>
-              );
-            }}
-          />
-        </Col>
-      </Row>
-    </Modal>
+                  新建清单
+                </Button>
+              )}
+            </div>
+
+            <div style={{ width: '100%' }}>
+              <Table
+                style={{ maxHeight: '90%' }}
+                columns={columns}
+                dataSource={data}
+                loading={nodeLoading}
+                bordered={false}
+                pagination={{ pageSize: 8, onChange }}
+                scroll={{ y: 65 * pageSize }}
+              />
+            </div>
+            {stepsData.map((item, idx) => (
+              <div key={`${item.name}_${idx}`} style={{ marginBottom: '20px' }}>
+                <div style={{ marginBottom: '4px' }}>{item.name}</div>
+                <Steps size="small" current={item.current} status={item.status}>
+                  {item.list.map(node => (
+                    <Step
+                      key={`${node.id}_${node.node}`}
+                      title={node.node}
+                      description={`审批人:${node.auditor || '-'}`}
+                    />
+                  ))}
+                </Steps>
+              </div>
+            ))}
+          </Col>
+        </Row>
+      </Modal>
+      <VersionModal
+        visible={versionVisible}
+        onClose={() => setVersionVisible(false)}
+        onOk={values => {
+          onCommit?.(values);
+          setVersionVisible(false);
+        }}
+      />
+    </>
   );
 }
 
@@ -130,6 +350,8 @@ const getColor = item => {
   return color;
 };
 
-export default connect(({ xflow, detail }) => ({
-  flowDetail: xflow.flowDetail,
+export default connect(({ loading, user }) => ({
+  loading,
+  currentUser: user.currentUser,
 }))(FlowModal);
+// export default FlowModal;

+ 112 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/HistoryDrawer.js

@@ -0,0 +1,112 @@
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Form } from '@ant-design/compatible';
+import '@ant-design/compatible/assets/index.css';
+import { Drawer, Descriptions, Card, Table, Timeline, Button, Space } from 'antd';
+import moment from 'moment';
+import { connect } from 'dva';
+import CommentContent from '@/components/CommentContent';
+import TimelineItem from 'antd/lib/timeline/TimelineItem';
+
+// 评论
+function HistoryDrawer(props) {
+  const {
+    flowDetail,
+    visible,
+    onClose,
+    version,
+    loading,
+    dispatch,
+    versionTree,
+    onChangeVersion,
+  } = props;
+  const columns = useMemo(() => {
+    return [
+      {
+        title: '名称',
+        render: item => (
+          <div style={{ color: '#9b9b9b' }}>
+            {item.version_no ? `${item.version_name}.${item.version_no}` : item.version_name}
+          </div>
+        ),
+      },
+      {
+        title: '业务节点',
+        width: '20%',
+        render: item => {
+          let node = flowDetail.Nodes.find(cur => cur.Id == item.template_node_id);
+          return <span style={{ color: '#9b9b9b' }}>{node?.label ? node.label : '-'}</span>;
+        },
+      },
+      {
+        title: '提交人',
+        width: '20%',
+        render: item => {
+          return (
+            <span style={{ color: '#9b9b9b' }}>
+              {item.AuthorInfo?.UserName ? item.AuthorInfo?.UserName : '-'}
+            </span>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: '20%',
+        render: item =>
+          item.id != version.id && (
+            <a
+              onClick={() => {
+                onChangeVersion(item);
+                onClose();
+              }}
+            >
+              加载
+            </a>
+          ),
+      },
+    ];
+  }, [version]);
+
+  const TimeLineItemRender = item => {
+    let time = item.m_time ? item.m_time.split('T')[0] : '-';
+    let node = flowDetail.Nodes.find(cur => cur.Id == item.template_node_id);
+    return (
+      <TimelineItem>
+        <Space>
+          <a
+            onClick={() => {
+              onChangeVersion(item);
+              onClose();
+            }}
+          >
+            {item.version_no ? `${item.version_name}.${item.version_no}` : item.version_name}
+          </a>
+          <span>{node?.label ? node.label : '-'}</span>
+          <span>提交人: {item.AuthorInfo?.UserName ? item.AuthorInfo?.UserName : '-'}</span>
+          <span>时间: {time}</span>
+        </Space>
+      </TimelineItem>
+    );
+  };
+
+  return (
+    <Drawer
+      width={600}
+      title="历史版本"
+      mask={false}
+      placement="right"
+      onClose={onClose}
+      visible={visible}
+    >
+      {/* <Table columns={columns} dataSource={versionTree} bordered={false} /> */}
+      <Timeline>{versionTree.map(item => TimeLineItemRender(item))}</Timeline>
+    </Drawer>
+  );
+}
+export default connect(({ detail, xflow, user, loading }) => ({
+  comment: detail.comment,
+  userList: user.list,
+  bomComment: detail.bomComment,
+  flowDetail: xflow.flowDetail,
+  loading: loading,
+}))(HistoryDrawer);

+ 267 - 62
src/pages/PurchaseAdmin/PurchaseList/Detail/Index.js

@@ -20,7 +20,11 @@ import CommitAuditModal from './CommitAuditModal';
 import CommentContent from '@/components/CommentContent';
 import MergeModal from './MergeModal';
 import { GetTokenFromUrl, getToken } from '@/utils/utils';
-import { queryDetail } from '@/services/boom';
+import { queryDetail, queryDingInstanceExecute } from '@/services/boom';
+import HistoryDrawer from './HistoryDrawer';
+import AuditFlow from './AuditFlow';
+import { getCurrentUser } from '@/utils/authority';
+import { async } from '@antv/x6/es/registry/marker/async';
 const LocalData = localStorage.luckysheet;
 
 const { Option } = Select;
@@ -38,8 +42,11 @@ function Detail(props) {
     versionList,
     auditList,
     flowDetail,
+    versionTree,
     match: { params },
+    instanceDetail,
   } = props;
+  const [versionTreeVisible, setVersionTreeVisible] = useState(false);
   const [commentVisible, setCommentVisible] = useState(false);
   const [mergeVisible, setMergeVisible] = useState(false);
   const [compareVisible, setCompareVisible] = useState(false);
@@ -65,6 +72,7 @@ function Detail(props) {
   const sheetRef2 = useRef();
   const sheetRef3 = useRef();
   const fileRef = useRef();
+  const userRef = useRef();
   const statusRef = useRef({
     edit: false,
     compare: false,
@@ -74,6 +82,33 @@ function Detail(props) {
   const projectId = parseInt(params.projectId);
   const templateId = parseInt(params.templateId);
 
+  const auditDetail = useMemo(() => {
+    let data = {
+      processCode: '',
+      deptId: '14169890',
+      tasks: [],
+      // userId: '16569001414345099',
+      // deptId: currentUser.DingDepId || getCurrentUser()?.DingDepId,
+      userId: currentUser.DingUserId || getCurrentUser()?.DingUserId,
+      formComponentValues: [],
+      activityId: '',
+      cc_userids: [],
+      status: version.audit_status,
+    };
+    if (version?.flow_id && instanceDetail?.tasks && instanceDetail.tasks?.length > 0) {
+      let item = flowDetail.nodes.find(item => item.Id == version.template_node_id);
+      if (!item) return data;
+      const { tasks, form_component_values, cc_userids } = instanceDetail;
+      data.processCode = item.process_code;
+      data.activityId = tasks[tasks.length - 1]?.activity_id;
+      data.tasks = tasks || [];
+      data.cc_userids = cc_userids;
+      data.formComponentValues = form_component_values?.filter(item => item.name);
+      if (version.status == 1) data.status = -1;
+    }
+    return data;
+  }, [instanceDetail, version]);
+
   const flow = useMemo(() => {
     let data = {
       active: 0,
@@ -94,17 +129,17 @@ function Detail(props) {
       item.currentNode = item.list.FlowNodes[item.current];
       data = item;
     }
-
     return data;
   }, [auditList, version]);
   const active_audit = flow.active_audit;
-
+  console.log(flow);
   const isAuditor = useMemo(() => {
     const getUserRole = () => {
-      let roleID = flow.currentNode?.AuditRoleInfo?.ID;
-      let item = currentUser.Permissions.find(role => role.RoleId == roleID);
-      if (item) return true;
-      return false;
+      return flow.currentNode.auditor == currentUser.ID;
+      // let roleID = flow.currentNode?.AuditRoleInfo?.ID;
+      // let item = currentUser.roleList.find(role => role.ID == roleID);
+      // if (item) return true;
+      // return false;
     };
     return active_audit == 1 && getUserRole();
   }, [active_audit, flow, currentUser]);
@@ -224,9 +259,7 @@ function Detail(props) {
         item => item.audit_status != 4 && item.template_node_id == currentNode.Id
       );
       if (serviceVersion) {
-        message.error(
-          `新建清单失败!业务节点【${currentNode.label}】为单清单节点。已存在清单【${serviceVersion.version_name}】。`
-        );
+        message.error(`新建清单失败!业务节点【${currentNode.label}】只能有一个清单!`);
         return;
       }
     }
@@ -248,6 +281,7 @@ function Detail(props) {
       new_version: '0',
       audit_status: 0,
       data: JSON.stringify(sheets),
+      base_id: version.id,
     };
     dispatch({
       type: 'detail/commitSheet',
@@ -288,11 +322,11 @@ function Detail(props) {
   };
 
   const onAudit = ({ audit_comment }) => {
-    var flowNode = flow.currentNode;
+    const flowNode = flow.currentNode;
     dispatch({
       type: 'detail/approve',
       payload: {
-        id: version.id,
+        id: flow.active_id,
         project_id: projectId,
         audit_status: 2,
         flow_id: flowNode.flow_id,
@@ -318,27 +352,6 @@ function Detail(props) {
     });
   };
 
-  const onSubmitAudit = () => {
-    Modal.confirm({
-      title: '提示',
-      content: '是否确认提交审批。',
-      okText: '确定',
-      cancelText: '取消',
-      onOk() {
-        var flowNode = flow.currentNode;
-        dispatch({
-          type: 'detail/submitAudit',
-          payload: {
-            id: version.id,
-            flow_id: flowNode.flow_id,
-            node_id: flowNode.id,
-            projectId,
-          },
-        });
-      },
-    });
-  };
-
   const onApprove = flag => {
     if (!flag) {
       setAuditVisible(true);
@@ -348,8 +361,31 @@ function Detail(props) {
     let isSingle = false;
     let serviceNode;
     const flowNode = flow.currentNode;
+    const getLastTemplateNodeId = data => {
+      let result;
+      const getFun = item => {
+        if (item.flow_path?.length > 0) {
+          getFun(item.flow_path[0]);
+        } else {
+          result = item.template_node_id;
+        }
+      };
+      if (!data) return version.template_node_id;
+      getFun(data[0]);
+      return result;
+    };
+    let lastTemplateNodeId = version.template_node_id;
+    if (version.flow_path) {
+      //如果多节点审批  获取当前是否审批流程的最后一个审批节点
+      let flowPathList = JSON.parse(version.flow_path);
+      lastTemplateNodeId = getLastTemplateNodeId(flowPathList);
+    }
+
     // 判断是否为最后一个审批节点
-    if (flow.current == flow.list.FlowNodes.length - 1) {
+    if (
+      lastTemplateNodeId == version.template_node_id &&
+      flow.current == flow.list.FlowNodes.length - 1
+    ) {
       serviceNode = flowDetail.nodes.find?.(item => item.Id == version.next_template_node_id);
       if (!serviceNode.muti_version) {
         //audit_status=4 表示为清单推进后留存的副本.不计入多清单计算
@@ -383,14 +419,25 @@ function Detail(props) {
                 id: templateId,
               },
             });
+
+            // 更新审批流
+            dispatch({
+              type: 'detail/queryAuditList',
+              payload: {
+                template_id: version.template_id,
+                template_node_id: version.template_node_id,
+                flow_id: version.flow_id,
+                version_id: version.version_id,
+              },
+            });
             if (flow.current == flow.list.FlowNodes.length - 1) {
-              // 最后一个审核节点通过后  需要更新version id
-              localStorage.excelId = newVersion.id;
-              setVersion({
-                ...version,
-                flow_id: newVersion.flow_id,
-                id: newVersion.id,
-              });
+              // 最后一个审核节点通过后  需要更新version id 不更不更,留在原地
+              // localStorage.excelId = newVersion.id;
+              // setVersion({
+              //   ...version,
+              //   flow_id: newVersion.flow_id,
+              //   id: newVersion.id,
+              // });
             }
           },
         });
@@ -398,6 +445,95 @@ function Detail(props) {
     });
   };
 
+  const onApprove1 = (flag, taskId) => {
+    const callback = () => {
+      // 更新flow流程图
+      dispatch({
+        type: 'xflow/queryBoomFlowDetail',
+        payload: {
+          id: templateId,
+        },
+      });
+
+      // 更新审批流
+      dispatch({
+        type: 'detail/queryDingInstanceDetail',
+        payload: {
+          process_instance_id: version.ding_instance_id, //创建表单成功返回的id
+        },
+      });
+    };
+
+    const request = async () => {
+      let param = {
+        request: {
+          process_instance_id: version.ding_instance_id,
+          result: flag ? 'agree' : 'refuse',
+          actioner_userid: currentUser.DingUserId || getCurrentUser()?.DingUserId,
+          task_id: taskId,
+        },
+      };
+      let res = await queryDingInstanceExecute(param);
+      if (res?.data?.result) {
+        callback();
+      }
+    };
+
+    let tipText = '是否通过审批。';
+    if (!flag) {
+      tipText = '是否拒绝审批。';
+      // request();
+      // return;
+    }
+
+    let isSingle = false;
+    let serviceNode;
+    const flowNode = flow.currentNode;
+    const getLastTemplateNodeId = data => {
+      let result;
+      const getFun = item => {
+        if (item.flow_path?.length > 0) {
+          getFun(item.flow_path[0]);
+        } else {
+          result = item.template_node_id;
+        }
+      };
+      if (!data) return version.template_node_id;
+      getFun(data[0]);
+      return result;
+    };
+    let lastTemplateNodeId = version.template_node_id;
+    if (version.flow_path) {
+      //如果多节点审批  获取当前是否审批流程的最后一个审批节点
+      let flowPathList = JSON.parse(version.flow_path);
+      lastTemplateNodeId = getLastTemplateNodeId(flowPathList);
+    }
+
+    // 判断是否为最后一个审批节点
+    if (
+      lastTemplateNodeId == version.template_node_id &&
+      flow.current == flow.list.FlowNodes.length - 1
+    ) {
+      serviceNode = flowDetail.nodes.find?.(item => item.Id == version.next_template_node_id);
+      if (!serviceNode.muti_version) {
+        //audit_status=4 表示为清单推进后留存的副本.不计入多清单计算
+        isSingle = versionList.find(
+          item => item.audit_status != 4 && item.template_node_id == serviceNode.Id
+        );
+        if (isSingle) tipText = `节点【${serviceNode.label}】只能拥有一个清单,是否覆盖?`;
+      }
+    }
+    Modal.confirm({
+      title: '提示',
+      content: tipText,
+      okText: '确定',
+      cancelText: '取消',
+      onOk: () => {
+        request();
+      },
+    });
+  };
+
   const onMerge = () => {
     const [sheet1, sheet2] = compareList;
     Modal.confirm({
@@ -442,6 +578,7 @@ function Detail(props) {
     //   sheetRef3.current.mergeExcl(sheet.data);
     // }, 400);
   };
+
   const handleClickFile = () => {
     fileRef.current.click();
   };
@@ -504,10 +641,6 @@ function Detail(props) {
         setCommitVisible(true);
         setCommentVisible(false);
         break;
-      case 'approval':
-        // 申请审批
-        onSubmitAudit();
-        break;
       case 'attachment':
         // 附件
         setFileVisible(true);
@@ -544,7 +677,22 @@ function Detail(props) {
     ];
     // version.audit_status:4 为副本。不可操作
     if (version.audit_status != 4) {
-      menuList.push(<Menu.Item key="commitAudit">提交流转</Menu.Item>);
+      //判断权限配置,如果配置了,就指定权限的人可提交,没配置就全部人都可提交
+      const getIsSubmit = () => {
+        const nodeId = version.template_node_id;
+        if (!flowDetail?.nodes || !nodeId) return;
+        const node = flowDetail.nodes.find(item => item.Id == nodeId);
+        if (!node || node.name == 'custom-circle') return;
+        if (node?.role_list) {
+          return node.role_list
+            .split(',')
+            .some(id => currentUser.roleList?.find(role => role.ID == id));
+        }
+        return true;
+      };
+      // console.log('是否有权限提交流转   ', getIsSubmit());
+      if (getIsSubmit() && version.audit_status != 3)
+        menuList.push(<Menu.Item key="commitAudit">提交流转</Menu.Item>);
 
       if (!isAuditor && canEdit() && !version.flow_id) {
         // menuList.push(<Menu.Item key="edit">编辑</Menu.Item>);
@@ -574,7 +722,9 @@ function Detail(props) {
     let item = '';
     switch (active_audit) {
       case 0:
-        return;
+        if (!flow.list || flow.list.FlowNodes?.length == 0) return;
+        item = <Alert message="审批拒绝" type="error" />;
+        break;
       case 1:
         item = <Alert message="等待审核中" type="info" />;
         break;
@@ -714,12 +864,20 @@ function Detail(props) {
     if (typeof id == 'object') {
       version = id;
       localStorage.excelId = version.id;
+      localStorage.excelItem = JSON.stringify(version);
     } else {
       version = versionList.find(item => item.id == id);
       if (!version) return;
       localStorage.excelId = id;
     }
     setVersion(version);
+    //请求历史版本
+    dispatch({
+      type: 'detail/queryVersionsTree',
+      payload: {
+        excel_id: version.id || localStorage.excelId,
+      },
+    });
 
     // 判断是否审批节点
     if (version.flow_id) {
@@ -742,12 +900,12 @@ function Detail(props) {
       callback: newVersion => {
         setCommitAuditVisible(false);
         // 更新version
-        localStorage.excelId = newVersion.id;
-        setVersion({
-          ...version,
-          flow_id: newVersion.flow_id,
-          id: newVersion.id,
-        });
+        // localStorage.excelId = newVersion.id;
+        // changeVersion({
+        //   ...version,
+        //   ...newVersion,
+        //   version_id: version.id
+        // });
         // 更新flow流程图
         dispatch({
           type: 'xflow/queryBoomFlowDetail',
@@ -759,15 +917,26 @@ function Detail(props) {
     });
   };
 
-  const getUser = user => {
-    setUser(user);
+  const getUser = newUser => {
+    try {
+      if (JSON.stringify(newUser) != JSON.stringify(userRef.current)) {
+        userRef.current = newUser;
+        setUser(newUser);
+      }
+    } catch (error) {}
   };
 
   const renderNode = () => {
     const nodeId = version.template_node_id;
     if (!flowDetail?.nodes || !nodeId) return;
     const node = flowDetail.nodes.find(item => item.Id == nodeId);
-    return `当前清单:${version.version_name || '-'};    当前节点:${node?.label || '-'}`;
+    // return `当前清单:${version.version_name || '-'};    当前节点:${node?.label || '-'}`;
+    return (
+      <span className={styles.curTitle}>
+        当前清单: <span>{version.version_name || '-'}</span>当前节点:{' '}
+        <span>{node?.label || '-'}</span>
+      </span>
+    );
   };
 
   const handleSubmitCell = (value, callback) => {
@@ -798,6 +967,15 @@ function Detail(props) {
     dispatch({
       type: 'user/fetch',
     });
+    dispatch({
+      type: 'user/fetchDepV2',
+    });
+    dispatch({
+      type: 'detail/queryListParentByUser',
+      payload: {
+        userid: currentUser.DingUserId || getCurrentUser()?.DingUserId,
+      },
+    });
   }, []);
 
   useEffect(() => {
@@ -818,7 +996,9 @@ function Detail(props) {
   useEffect(() => {
     if (versionList.length == 0) return;
     if (!version.id) {
-      const excelId = localStorage.excelId;
+      const excelId = localStorage.excelItem
+        ? JSON.parse(localStorage.excelItem)
+        : localStorage.excelId;
       changeVersion(excelId);
     } else {
       changeVersion(version.id);
@@ -839,17 +1019,27 @@ function Detail(props) {
               新建清单
             </Button>
           )}
-          <span style={{ marginLeft: 20 }}>{renderNode()}</span>
+          {renderNode()}
         </div>
         <div className={styles.btns}>
+          <Button
+            type="primary"
+            style={{ marginRight: 20 }}
+            onClick={() => setVersionTreeVisible(true)}
+          >
+            历史版本
+          </Button>
           <Avatar.Group style={{ marginRight: 20 }}>
-            {user.map(item => (
-              <Avatar style={{ backgroundColor: '#008dff' }} size="large">
+            {user.map((item, id) => (
+              <Avatar
+                key={`${id}-${item.name}`}
+                style={{ backgroundColor: '#008dff' }}
+                size="large"
+              >
                 {item.Name}
               </Avatar>
             ))}
           </Avatar.Group>
-
           {renderBtns()}
         </div>
         <input
@@ -860,8 +1050,11 @@ function Detail(props) {
         />
       </div>
       <TimeNode flow={flow} isAuditor={isAuditor} onApprove={onApprove}></TimeNode>
+      {/* {version.flow_id ? (
+        <AuditFlow {...auditDetail} canShowAudit={true} onApprove={onApprove} />
+      ) : null} */}
 
-      {renderAlert()}
+      {/* {renderAlert()} */}
       {/* 判断是否为比对模式 */}
       {compareList.length == 2 ? (
         <>
@@ -887,6 +1080,14 @@ function Detail(props) {
         </div>
       )}
 
+      <HistoryDrawer
+        versionTree={versionTree}
+        version={version}
+        visible={versionTreeVisible}
+        onChangeVersion={version => changeVersion(version)}
+        onClose={() => setVersionTreeVisible(false)}
+      />
+
       <CommentContent
         title="单元格沟通记录"
         comment={comment}
@@ -918,6 +1119,7 @@ function Detail(props) {
         onOk={downloadExcel}
       />
       <FlowModal
+        flowDetail={flowDetail}
         visible={flowVisible}
         onClose={() => setFlowVisible(false)}
         version={version}
@@ -950,6 +1152,7 @@ function Detail(props) {
         version={version}
         onClose={() => setCommitAuditVisible(false)}
         onOk={onSubmitNextNode}
+        luckysheet={sheetRef}
       />
     </Spin>
   );
@@ -961,8 +1164,10 @@ export default connect(({ detail, user, xflow, loading }) => ({
   fileList: detail.fileList,
   history: detail.history,
   comment: detail.comment,
+  instanceDetail: detail.dingInstanceDetail,
   currentUser: user.currentUser,
   roleList: detail.roleList,
   versionList: detail.versionList,
+  versionTree: detail.versionTree,
   loading,
 }))(Detail);

+ 10 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/Index.less

@@ -47,3 +47,13 @@
 .historyList {
   padding: 0 10px;
 }
+.curTitle  {
+  margin-left: 20px;
+ span {
+  color: #1890FF;
+  margin-right: 10px;
+ }
+}
+.topF{
+  margin: 20px;
+}

+ 24 - 15
src/pages/PurchaseAdmin/PurchaseList/Detail/LuckySheet.js

@@ -1,6 +1,6 @@
 import React from 'react';
 import { message } from 'antd';
-import exportExcel from '@/utils/exportExcl';
+import exportExcel, { getExcelBolob } from '@/utils/exportExcl';
 import LuckyExcel from 'luckyexcel';
 import { getToken } from '@/utils/utils';
 
@@ -116,16 +116,17 @@ class LuckySheet extends React.Component {
         //   }, 300);
         // },
       };
-      if(version.flow_id) {
+      console.log(version);
+      if (version.flow_id) {
         option.authority = {
           sheet: true,
-          hintText: '当前处于审批节点,禁止编辑!'
-        }
-      } else if(version.audit_status == 4) {
+          hintText: '当前处于审批节点,禁止编辑!',
+        };
+      } else if (version.audit_status != 0 || version.status == 1) {
         option.authority = {
           sheet: true,
-          hintText: '当前清单为历史清单,不可编辑!'
-        }
+          hintText: '当前清单不可编辑!',
+        };
       }
     } else if (data && data.length > 0) {
       option.data = JSON.parse(JSON.stringify(data));
@@ -438,7 +439,7 @@ class LuckySheet extends React.Component {
   //   });
   // }
 
-  downloadExcel(checkValue) {
+  getExcelData(checkValue = null) {
     let resultList = [];
     console.log(this.luckysheet.getAllSheets());
 
@@ -446,24 +447,21 @@ class LuckySheet extends React.Component {
     currentData.forEach(sheet => {
       let data = sheet.data;
       let celldata = sheet.celldata;
-      //获取当前导出列的数组
       let colList = [];
       data[0]?.forEach((rowOneItem, colIdx) => {
-        if (rowOneItem && checkValue.indexOf(rowOneItem.v) !== -1) {
-          colList.indexOf(colIdx) == -1 ? colList.push(colIdx) : true;
+        if (rowOneItem) {
+          if (!checkValue || checkValue.indexOf(rowOneItem.v) !== -1) {
+            colList.indexOf(colIdx) == -1 ? colList.push(colIdx) : true;
+          }
         }
       });
 
-      //处理data
       const newData = [];
       data.forEach(item => {
         if (item !== null) {
           let arr = item.filter((cur, idx) => {
             return item && colList.includes(idx);
           });
-          // let len = item.length - arr.length
-          // let nullList = new Array(len).fill(null);
-          // let rowList =  arr.concat(nullList);
           newData.push(arr);
         }
       });
@@ -484,6 +482,17 @@ class LuckySheet extends React.Component {
       });
       sheet.celldata = newCellData;
     });
+
+    return currentData;
+  }
+
+  getExcelBolb() {
+    let currentData = this.getExcelData();
+    return getExcelBolob(currentData);
+  }
+
+  downloadExcel(checkValue) {
+    let currentData = this.getExcelData(checkValue);
     exportExcel(currentData, '下载');
   }
 

+ 8 - 4
src/pages/PurchaseAdmin/PurchaseList/Detail/TimeNode.js

@@ -14,20 +14,24 @@ function TimeNode(props) {
     onApprove,
   } = props;
 
-  if (active !== 0) {
+  if (!list || list.FlowNodes?.length != 0) {
     return (
       <div className={styles.top}>
-        <Steps current={current}>
+        <Steps current={current} status={active == 0 ? 'error' : 'process'}>
           {list.FlowNodes.map(item => (
             <Step
               key={item.id}
               title={item.node}
-              description={`审批角色:${item?.AuditRoleInfo.Name || '-'}`}
+              description={
+                item?.AuditRoleInfo
+                  ? `审批人:${item?.AuditRoleInfo.Name || '-'}`
+                  : `审批人:${item?.AuditorUser.CName || '-'}`
+              }
             />
           ))}
         </Steps>
         <div className={styles.btns} style={{ marginLeft: 80 }}>
-          {isAuditor && (
+          {isAuditor && active != 0 && (
             <>
               <Button type="primary" onClick={() => onApprove(true)}>
                 审批通过

+ 76 - 2
src/pages/PurchaseAdmin/PurchaseList/Detail/models/detail.js

@@ -24,10 +24,17 @@ import {
   queryHistoryList,
   queryHistoryDetail,
   submitNextNode,
+  advanceSubmitNextNode,
   addBomComment,
   queryBomComment,
   approve,
   queryAuthority,
+  queryVersionsTree,
+  queryAuditExcel,
+  queryAuditRecord,
+  queryDingSchema,
+  queryDingInstanceDetail,
+  queryListParentByUser,
 } from '@/services/boom';
 import { queryRole } from '@/services/SysAdmin';
 import { setCurrentUser } from '@/utils/authority';
@@ -68,6 +75,9 @@ export default {
     fileList: [],
     roleList: [],
     authority: [],
+    versionTree: [],
+    auditExcel: [],
+    dingInstanceD: [],
   },
 
   effects: {
@@ -162,8 +172,8 @@ export default {
             // template_id: payload.template_id,
             // template_node_id: payload.template_node_id,
           },
-          callback: (versionList) => {
-            let newVersion = versionList.find(item => item.id == response.data.id)
+          callback: versionList => {
+            let newVersion = versionList.find(item => item.id == response.data.id);
             callback && callback(newVersion);
           },
         });
@@ -214,6 +224,25 @@ export default {
         });
       }
     },
+    // 实时计算审批人
+    *advanceSubmitNextNode({ payload, callback }, { call, put }) {
+      let response = yield call(advanceSubmitNextNode, payload);
+      if (response) {
+        console.log(response);
+        callback?.(response.data);
+
+        // message.success('提交成功');
+        // yield put({
+        //   type: 'queryVersionsList',
+        //   payload: {
+        //     project_id: payload.project_id,
+        //   },
+        //   callback: () => {
+        //     callback(response.data);
+        //   },
+        // });
+      }
+    },
     *queryDetail({ payload, callback }, { call, put }) {
       const sheet = yield call(queryDetail, payload);
       if (sheet) {
@@ -487,6 +516,51 @@ export default {
         });
       }
     },
+    *queryVersionsTree({ payload }, { call, put }) {
+      const response = yield call(queryVersionsTree, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { versionTree: response.data },
+        });
+      }
+    },
+    *queryAuditExcel({ payload, callback }, { call, put }) {
+      const response = yield call(queryAuditExcel, payload);
+      if (response) {
+        callback && callback(response.data?.all);
+      }
+    },
+    *queryAuditRecord({ payload, callback }, { call, put }) {
+      const response = yield call(queryAuditRecord, payload);
+      if (response) {
+        callback && callback(response.data?.all);
+      }
+    },
+    *queryDingSchema({ payload, callback }, { call, put }) {
+      const response = yield call(queryDingSchema, payload);
+      if (response) {
+        callback && callback(response.data?.all);
+      }
+    },
+    *queryDingInstanceDetail({ payload, callback }, { call, put }) {
+      const response = yield call(queryDingInstanceDetail, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { dingInstanceDetail: response.data?.process_instance },
+        });
+      }
+    },
+    *queryListParentByUser({ payload, callback }, { call, put }) {
+      const response = yield call(queryListParentByUser, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { parentDepList: response.data?.result },
+        });
+      }
+    },
   },
 
   reducers: {

+ 85 - 213
src/pages/PurchaseAdmin/PurchaseList/Flow/Audit.js

@@ -1,239 +1,111 @@
-import React, { useState, useEffect } from 'react';
-import { Form, Select, Button, Table, Input, Checkbox, Divider } from 'antd';
+import React, { useState, useEffect, useRef, useMemo } from 'react';
+import { Form, Select, Button, Table, Input, Checkbox, Divider, Tabs } from 'antd';
 import { connect } from 'dva';
 import AuditNodeModal from './AuditNodeModal';
 import AuditModal from './AuditModal';
 import styles from './Audit.less';
 import router from 'umi/router';
-
+import Flow, { FLOW_TYPE } from '@/components/Flow';
+import AuditForm from '@/components/AuditForm';
+import { async } from '@antv/x6/lib/registry/marker/async';
 const { Option } = Select;
+const { TabPane } = Tabs;
+
+const FLOWID = 2;
 
 function Audit(props) {
-  const { roleList, currentItem, dispatch } = props;
-  const [visible, setVisible] = useState({
-    audit: false,
-    auditNode: false,
-  });
-  const [current, setCurrent] = useState({ ...currentItem });
-  const [currentNode, setCurrentNode] = useState({});
-  const columns = [
-    {
-      title: '节点名',
-      width: "20%",
-      dataIndex: 'node',
-    },
-    // {
-    //   title: '审批级别',
-    //   dataIndex: 'seq',
-    //   width: "20%",
-    // },
-    {
-      title: '审批角色',
-      dataIndex: 'audit_role',
-      width: "20%",
-      render: audit_role => roleList.find(item => item.ID == audit_role)?.Name,
-    },
-    // {
-    //   title: '审批关系',
-    //   width: "20%",
-    //   dataIndex: 'seq_relate',
-    //   render: relate => {
-    //     switch (relate) {
-    //       case 0:
-    //         return '无';
-    //       case 1:
-    //         return '或';
-    //       case 2:
-    //         return '并';
-    //     }
-    //   },
-    // },
-    {
-      title: '操作',
-      width: "20%",
-      render: item => (
-        <>
-          <a
-            onClick={() => {
-              setCurrentNode(item);
-              changeVisible('auditNode', true);
-            }}
-          >
-            编辑
-          </a>
-          <Divider type="vertical" />
-          <a
-            onClick={() => {
-              handleDelete(item);
-            }}
-          >
-            删除
-          </a>
-        </>
-      ),
-    },
-  ];
-  // const handleAuditOk = values => {
-  //   console.log(values);
-  //   dispatch({
-  //     type: 'flow/addAudit',
-  //     payload: values,
-  //     callback: () => {
-  //       changeVisible('audit', false);
-  //     },
-  //   });
-  // };
-  const handleAuditNodeOk = values => {
-    console.log(values);
-    let FlowNodes = current.FlowNodes;
+  const {
+    roleList,
+    currentItem,
+    dispatch,
+    formItems,
+    formData,
+    flowDetail,
+    simpleFlowDteail,
+  } = props;
+  const ref = useRef();
 
-    changeVisible('auditNode', false);
-    if (currentNode.id) {
-      let index = FlowNodes.findIndex(item => item.id == currentNode.id);
-      let newNodes = [...FlowNodes];
-      newNodes[index] = values;
-      // 将同审批级别的审批关系设置为相同
-      newNodes.forEach(item => {
-        if (item.seq == values.seq) {
-          item.seq_relate = values.seq_relate;
-        }
-      });
-      // 排序
-      newNodes.sort((a, b) => a.seq - b.seq);
-      setCurrent({
-        ...current,
-        FlowNodes: newNodes,
-      });
-    } else {
-      setCurrent({
-        ...current,
-        FlowNodes: [...FlowNodes, values],
-      });
-    }
-  };
-  const handleDelete = item => {
-    let newNodes = [...current.FlowNodes];
-    let index = newNodes.indexOf(item);
-    newNodes.splice(index, 1);
-    setCurrent({
-      ...current,
-      FlowNodes: newNodes,
+  const curItem = useMemo(() => {
+    let item = localStorage.getItem('currentAudit');
+    return JSON.stringify(currentItem) == '{}' ? JSON.parse(item) : currentItem;
+  }, [currentItem, localStorage.getItem('currentAudit')]);
+
+  useEffect(() => {
+    dispatch({
+      type: 'flow/queryProcessFlows',
+      payload: { ids: Number(curItem.id) },
+    });
+    dispatch({
+      type: 'user/getRoleList',
+    });
+    dispatch({
+      type: 'user/fetch',
     });
-  };
-  // const onCancel = () => {
-  //   form.resetFields();
-  //   setCurrent({});
-  // };
-  const onSave = () => {
-    const nodes = current.FlowNodes.map((item,index) => ({
-      flow_id: current.id,
-      node: item.node,
-      desc: item.desc,
-      audit_role: item.audit_role,
-      seq: index + 1,
-      // seq_relate: item.seq_relate,
-    }));
 
     dispatch({
-      type: 'flow/addAuditNode',
+      type: 'user/fetchDepV2',
+    });
+  }, []);
+  const onChange = values => {
+    dispatch({
+      type: 'xflow/save',
       payload: {
-        flowId: current.id,
-        nodes: nodes,
+        formData: values,
       },
     });
   };
-  // const handleSelect = id => {
-  //   const item = list.find(item => item.list?.id == id);
-  //   console.log(item);
-  //   setCurrent({
-  //     ...item.list,
-  //   });
-  // };
-  const changeVisible = (type, visible) => {
-    setVisible({
-      ...visible,
-      [type]: visible,
-    });
-  };
-
-  // const renderTitle = () => {
-  //   return (
-  //     <div className={styles.box}>
-  //       <span>审批详情</span>
-  //       {current?.id && (
-  //         <div className={styles.btns}>
-  //           {/* <Button onClick={onCancel}>取消</Button> */}
-  //           <Button onClick={onSave} type="primary">
-  //             保存
-  //           </Button>
-  //           <Button
-  //             onClick={() => {
-  //               setCurrentNode({});
-  //               changeVisible('auditNode', true);
-  //             }}
-  //             type="primary"
-  //           >
-  //             添加节点
-  //           </Button>
-  //         </div>
-  //       )}
-  //     </div>
-  //   );
-  // };
 
-  useEffect(() => {
-    if (!current.id) {
-      router.go(-1);
-    } else {
-      dispatch({
-        type: 'user/fetch',
-      });
-      dispatch({
-        type: 'flow/queryAuditList',
-      });
-      dispatch({
-        type: 'flow/getRoleList',
-      });
+  const handleSaveClick = async () => {
+    //只修改表单不渲染xflow getGraphData方法找不到,保存接口返回的flowDetail数据
+    if (!ref.current?.getGraphData) {
+      let param = {
+        // name: curItem.name,
+        id: Number(curItem.id),
+        form_json: JSON.stringify(formItems),
+        process_json: JSON.stringify(flowDetail),
+        process_simple_json: simpleFlowDteail,
+      };
+      dispatch({ type: 'flow/saveAuditFlowInfo', payload: param });
+      return;
     }
-  }, []);
-
+    await ref.current?.getGraphData?.((data, simpleNodes) => {
+      let param = {
+        // name: curItem.name,
+        id: Number(curItem.id),
+        form_json: JSON.stringify(formItems),
+        process_json: data,
+        process_simple_json: simpleNodes,
+      };
+      dispatch({ type: 'flow/saveAuditFlowInfo', payload: param });
+    });
+  };
   return (
-    <div>
-      <div className={styles.btns}>
-        <Button onClick={() => router.go(-1)}>返回</Button>
-        {/* <Button onClick={onCancel}>取消</Button> */}
-        <Button onClick={onSave} type="primary">
-          保存
-        </Button>
-        <Button
-          onClick={() => {
-            setCurrentNode({});
-            changeVisible('auditNode', true);
-          }}
-          type="primary"
-        >
-          添加节点
-        </Button>
-      </div>
-
-      <Table rowKey="id" dataSource={current?.FlowNodes || []} columns={columns} />
-      {/* <AuditModal
-        visible={visible.audit}
-        onOk={handleAuditOk}
-        onCancel={() => changeVisible('audit', false)}
-      /> */}
-      <AuditNodeModal
-        roleList={roleList}
-        data={currentNode}
-        visible={visible.auditNode}
-        onOk={handleAuditNodeOk}
-        onCancel={() => changeVisible('auditNode', false)}
-      />
+    <div style={{ position: 'relative' }}>
+      <p>{curItem.name}</p>
+      <Tabs defaultActiveKey="1">
+        <TabPane tab="表单设计" key="1">
+          <AuditForm value={formData} onChange={values => onChange(values)} />
+        </TabPane>
+        <TabPane tab="流程控制" key="2">
+          <Flow meta={{ type: 'edit', flowId: curItem.id }} flowDetail={flowDetail} ref={ref} />
+        </TabPane>
+      </Tabs>
+      <Button
+        type="primary"
+        onClick={handleSaveClick}
+        style={{ position: 'absolute', right: 0, top: 0 }}
+      >
+        保存
+      </Button>
     </div>
   );
 }
-export default connect(({ flow, loading }) => ({
+export default connect(({ flow, loading, xflow }) => ({
   roleList: flow.roleList,
   currentItem: flow.current,
   loading: loading.models.purchaseList2,
+  formItems: xflow.formData,
+  flowDetail: flow.flowDetail,
+  formData: flow.formData,
+  simpleFlowDteail: flow.simpleFlowDteail,
 }))(Audit);

+ 8 - 11
src/pages/PurchaseAdmin/PurchaseList/Flow/AuditList.js

@@ -18,7 +18,7 @@ function Audit(props) {
   const columns = [
     {
       title: '审批流名称',
-      dataIndex: ['list','name'],
+      dataIndex: ['list', 'name'],
     },
     {
       title: '操作',
@@ -51,15 +51,16 @@ function Audit(props) {
       [type]: visible,
     });
   };
-  const setCurrentNode = (item) => {
+  const setCurrentNode = item => {
+    if (item?.list) localStorage.setItem('currentAudit', JSON.stringify(item.list));
     dispatch({
       type: 'flow/save',
       payload: {
         current: item.list,
-      }
-    })
-    router.push("/home/audit")
-  }
+      },
+    });
+    router.push('/home/audit');
+  };
 
   useEffect(() => {
     dispatch({
@@ -80,11 +81,7 @@ function Audit(props) {
         </Form>
       </div>
 
-      <Table
-        rowKey="id"
-        dataSource={list}
-        columns={columns}
-      />
+      <Table rowKey="id" dataSource={list} columns={columns} />
       <AuditModal
         visible={visible.audit}
         onOk={handleAuditOk}

+ 3 - 0
src/pages/PurchaseAdmin/PurchaseList/Flow/Flow.js

@@ -49,6 +49,9 @@ class FlowPage extends React.PureComponent {
     dispatch({
       type: 'xflow/queryAuditList',
     });
+    dispatch({
+      type: 'xflow/fetchDepV2',
+    });
 
     UnityAction.on('NODE_SAVE', nodeConfig => {
       this.onUpdate(nodeConfig);

+ 626 - 224
src/pages/PurchaseAdmin/PurchaseList/Flow/FlowDetail.json

@@ -10,10 +10,22 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "293a90b5" },
-          { "group": "right", "id": "92334433" },
-          { "group": "bottom", "id": "c2ab5849" },
-          { "group": "left", "id": "6079a903" }
+          {
+            "group": "top",
+            "id": "293a90b5"
+          },
+          {
+            "group": "right",
+            "id": "92334433"
+          },
+          {
+            "group": "bottom",
+            "id": "c2ab5849"
+          },
+          {
+            "group": "left",
+            "id": "6079a903"
+          }
         ]
       },
       "isCustom": true,
@@ -31,15 +43,27 @@
       "height": 90,
       "ports": {
         "items": [
-          { "group": "top", "id": "a61170c3" },
-          { "group": "right", "id": "821f59c0" },
-          { "group": "bottom", "id": "17360bc4" },
-          { "group": "left", "id": "15d1b217" }
+          {
+            "group": "top",
+            "id": "a61170c3"
+          },
+          {
+            "group": "right",
+            "id": "821f59c0"
+          },
+          {
+            "group": "bottom",
+            "id": "17360bc4"
+          },
+          {
+            "group": "left",
+            "id": "15d1b217"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": -369,
+      "x": -290,
       "y": -170,
       "zIndex": 10
     },
@@ -52,15 +76,27 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "4b4d9fa6" },
-          { "group": "right", "id": "ce88d7e2" },
-          { "group": "bottom", "id": "e69d8709" },
-          { "group": "left", "id": "c29d7b43" }
+          {
+            "group": "top",
+            "id": "4b4d9fa6"
+          },
+          {
+            "group": "right",
+            "id": "ce88d7e2"
+          },
+          {
+            "group": "bottom",
+            "id": "e69d8709"
+          },
+          {
+            "group": "left",
+            "id": "c29d7b43"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": -162,
+      "x": -138,
       "y": -150,
       "zIndex": 10
     },
@@ -73,10 +109,22 @@
       "height": 90,
       "ports": {
         "items": [
-          { "group": "top", "id": "331cd291" },
-          { "group": "right", "id": "ff6724ee" },
-          { "group": "bottom", "id": "16b4df46" },
-          { "group": "left", "id": "34c9dbc6" }
+          {
+            "group": "top",
+            "id": "331cd291"
+          },
+          {
+            "group": "right",
+            "id": "ff6724ee"
+          },
+          {
+            "group": "bottom",
+            "id": "16b4df46"
+          },
+          {
+            "group": "left",
+            "id": "34c9dbc6"
+          }
         ]
       },
       "isCustom": true,
@@ -94,10 +142,22 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "f71ce55b" },
-          { "group": "right", "id": "e67dc19c" },
-          { "group": "bottom", "id": "a06aba2c" },
-          { "group": "left", "id": "b578cc26" }
+          {
+            "group": "top",
+            "id": "f71ce55b"
+          },
+          {
+            "group": "right",
+            "id": "e67dc19c"
+          },
+          {
+            "group": "bottom",
+            "id": "a06aba2c"
+          },
+          {
+            "group": "left",
+            "id": "b578cc26"
+          }
         ]
       },
       "isCustom": true,
@@ -115,10 +175,22 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "0fc44196" },
-          { "group": "right", "id": "d2030f1b" },
-          { "group": "bottom", "id": "188c9b68" },
-          { "group": "left", "id": "4e9ce7ad" }
+          {
+            "group": "top",
+            "id": "0fc44196"
+          },
+          {
+            "group": "right",
+            "id": "d2030f1b"
+          },
+          {
+            "group": "bottom",
+            "id": "188c9b68"
+          },
+          {
+            "group": "left",
+            "id": "4e9ce7ad"
+          }
         ]
       },
       "isCustom": true,
@@ -136,15 +208,27 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "b58731c5" },
-          { "group": "right", "id": "b3dfbc16" },
-          { "group": "bottom", "id": "89c0bc16" },
-          { "group": "left", "id": "a3adcac9" }
+          {
+            "group": "top",
+            "id": "b58731c5"
+          },
+          {
+            "group": "right",
+            "id": "b3dfbc16"
+          },
+          {
+            "group": "bottom",
+            "id": "89c0bc16"
+          },
+          {
+            "group": "left",
+            "id": "a3adcac9"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": -384,
+      "x": -305,
       "y": -304,
       "zIndex": 10
     },
@@ -157,15 +241,27 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "7f8dc65b" },
-          { "group": "right", "id": "9a699e3f" },
-          { "group": "bottom", "id": "d348b56a" },
-          { "group": "left", "id": "47317157" }
+          {
+            "group": "top",
+            "id": "7f8dc65b"
+          },
+          {
+            "group": "right",
+            "id": "9a699e3f"
+          },
+          {
+            "group": "bottom",
+            "id": "d348b56a"
+          },
+          {
+            "group": "left",
+            "id": "47317157"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": -162,
+      "x": -138,
       "y": -304,
       "zIndex": 10
     },
@@ -178,15 +274,27 @@
       "height": 90,
       "ports": {
         "items": [
-          { "group": "top", "id": "cf1c4df0" },
-          { "group": "right", "id": "1eb352b0" },
-          { "group": "bottom", "id": "83b59198" },
-          { "group": "left", "id": "94f485b5" }
+          {
+            "group": "top",
+            "id": "cf1c4df0"
+          },
+          {
+            "group": "right",
+            "id": "1eb352b0"
+          },
+          {
+            "group": "bottom",
+            "id": "83b59198"
+          },
+          {
+            "group": "left",
+            "id": "94f485b5"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": 213,
+      "x": 356,
       "y": -324,
       "zIndex": 10
     },
@@ -199,15 +307,27 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "3ca1320c" },
-          { "group": "right", "id": "4dee75d9" },
-          { "group": "bottom", "id": "0f72f2ba" },
-          { "group": "left", "id": "bac7962b" }
+          {
+            "group": "top",
+            "id": "3ca1320c"
+          },
+          {
+            "group": "right",
+            "id": "4dee75d9"
+          },
+          {
+            "group": "bottom",
+            "id": "0f72f2ba"
+          },
+          {
+            "group": "left",
+            "id": "bac7962b"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": 198,
+      "x": 446,
       "y": -150,
       "zIndex": 10
     },
@@ -220,15 +340,27 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "04c81e99" },
-          { "group": "right", "id": "0d594eef" },
-          { "group": "bottom", "id": "17ff5fe6" },
-          { "group": "left", "id": "73307680" }
+          {
+            "group": "top",
+            "id": "04c81e99"
+          },
+          {
+            "group": "right",
+            "id": "0d594eef"
+          },
+          {
+            "group": "bottom",
+            "id": "17ff5fe6"
+          },
+          {
+            "group": "left",
+            "id": "73307680"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": 386,
+      "x": 621,
       "y": -150,
       "zIndex": 10
     },
@@ -241,15 +373,27 @@
       "height": 50,
       "ports": {
         "items": [
-          { "group": "top", "id": "2aae2a71" },
-          { "group": "right", "id": "e3a42bda" },
-          { "group": "bottom", "id": "0f06668a" },
-          { "group": "left", "id": "8e0bff55" }
+          {
+            "group": "top",
+            "id": "2aae2a71"
+          },
+          {
+            "group": "right",
+            "id": "e3a42bda"
+          },
+          {
+            "group": "bottom",
+            "id": "0f06668a"
+          },
+          {
+            "group": "left",
+            "id": "8e0bff55"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": 198,
+      "x": 446,
       "y": 26,
       "zIndex": 10
     },
@@ -257,20 +401,32 @@
       "id": "3fb8d302",
       "renderKey": "custom-circle",
       "name": "custom-circle",
-      "label": "三级审批",
+      "label": "三级审批4",
       "width": 90,
       "height": 90,
       "ports": {
         "items": [
-          { "group": "top", "id": "8ab6c3f6" },
-          { "group": "right", "id": "205f1437" },
-          { "group": "bottom", "id": "761ad2b5" },
-          { "group": "left", "id": "22d16375" }
+          {
+            "group": "top",
+            "id": "8ab6c3f6"
+          },
+          {
+            "group": "right",
+            "id": "205f1437"
+          },
+          {
+            "group": "bottom",
+            "id": "761ad2b5"
+          },
+          {
+            "group": "left",
+            "id": "22d16375"
+          }
         ]
       },
       "isCustom": true,
       "parentKey": "1",
-      "x": 42,
+      "x": 308,
       "y": -170,
       "zIndex": 10
     },
@@ -283,10 +439,22 @@
       "height": 90,
       "ports": {
         "items": [
-          { "group": "top", "id": "99f69f24" },
-          { "group": "right", "id": "0bccd839" },
-          { "group": "bottom", "id": "42c0d925" },
-          { "group": "left", "id": "58f52f2c" }
+          {
+            "group": "top",
+            "id": "99f69f24"
+          },
+          {
+            "group": "right",
+            "id": "0bccd839"
+          },
+          {
+            "group": "bottom",
+            "id": "42c0d925"
+          },
+          {
+            "group": "left",
+            "id": "58f52f2c"
+          }
         ]
       },
       "isCustom": true,
@@ -294,203 +462,437 @@
       "x": 42,
       "y": 6,
       "zIndex": 10
-    }
-  ],
-  "edges": [
+    },
     {
-      "id": "41561012:92334433-975bf288:15d1b217",
-      "source": { "cell": "41561012", "port": "92334433" },
-      "target": { "cell": "975bf288", "port": "15d1b217" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "id": "2427bf29",
+      "renderKey": "custom-circle",
+      "name": "custom-circle",
+      "label": "三级审批1",
+      "width": 90,
+      "height": 90,
+      "ports": {
+        "items": [
+          {
+            "group": "top",
+            "id": "e5a149c4"
+          },
+          {
+            "group": "right",
+            "id": "e1a1ecea"
+          },
+          {
+            "group": "bottom",
+            "id": "6e131e6a"
+          },
+          {
+            "group": "left",
+            "id": "6bbf9ae4"
+          }
+        ]
+      },
+      "isCustom": true,
+      "parentKey": "1",
+      "x": -425,
+      "y": -224,
+      "zIndex": 10
     },
     {
-      "id": "975bf288:821f59c0-5764f3ce:c29d7b43",
-      "source": { "cell": "975bf288", "port": "821f59c0" },
-      "target": { "cell": "5764f3ce", "port": "c29d7b43" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "id": "be25fe75",
+      "renderKey": "custom-circle",
+      "name": "custom-circle",
+      "label": "三级审批2",
+      "width": 90,
+      "height": 90,
+      "ports": {
+        "items": [
+          {
+            "group": "top",
+            "id": "13e4b9ea"
+          },
+          {
+            "group": "right",
+            "id": "ce651308"
+          },
+          {
+            "group": "bottom",
+            "id": "9a0b8942"
+          },
+          {
+            "group": "left",
+            "id": "a705e7ed"
+          }
+        ]
+      },
+      "isCustom": true,
+      "parentKey": "1",
+      "x": -425,
+      "y": -115,
+      "zIndex": 10
+    },
+    {
+      "id": "node-186a9d31-0bd3-4b36-b61f-6b5380c824db",
+      "renderKey": "custom-circle",
+      "name": "custom-circle",
+      "label": "审批节点1",
+      "width": 90,
+      "height": 90,
+      "ports": {
+        "items": [
+          {
+            "group": "top",
+            "id": "c6ec15a8-2db4-4ca8-ad81-fd1783b4ca0f"
+          },
+          {
+            "group": "right",
+            "id": "e134d65d-a197-4116-ad74-f47dbbb727d1"
+          },
+          {
+            "group": "bottom",
+            "id": "2c633cdf-7bb5-49ea-a4dc-e3d8c837e513"
+          },
+          {
+            "group": "left",
+            "id": "6d880b9d-7d7e-4357-94dd-caf7d73b5f80"
+          }
+        ]
+      },
+      "isCustom": true,
+      "parentKey": "1",
+      "x": 27,
+      "y": -224,
+      "zIndex": 10
     },
+    {
+      "id": "node-c5171e2d-1cd8-4019-83dc-9f2ed0cab6e8",
+      "renderKey": "custom-circle",
+      "name": "custom-circle",
+      "label": "审批节点2",
+      "width": 90,
+      "height": 90,
+      "ports": {
+        "items": [
+          {
+            "group": "top",
+            "id": "7b9fcd52-4fca-47c7-aaae-e3b117c0c234"
+          },
+          {
+            "group": "right",
+            "id": "6316d87e-2eb3-4ff6-bd8e-0cfd11bff4ce"
+          },
+          {
+            "group": "bottom",
+            "id": "38113945-040f-4374-aca8-38e2e81d71a8"
+          },
+          {
+            "group": "left",
+            "id": "8637b869-7924-416a-b938-30a7ca932901"
+          }
+        ]
+      },
+      "isCustom": true,
+      "parentKey": "1",
+      "x": 27,
+      "y": -115,
+      "zIndex": 10
+    },
+    {
+      "id": "node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862",
+      "renderKey": "custom-circle",
+      "name": "custom-circle",
+      "label": "审批节点3",
+      "width": 90,
+      "height": 90,
+      "ports": {
+        "items": [
+          {
+            "group": "top",
+            "id": "d9b27b16-08ab-4e0d-84dc-fdec64c0a129"
+          },
+          {
+            "group": "right",
+            "id": "1ab6b2a4-dccd-41e9-83c6-d1642c9ad844"
+          },
+          {
+            "group": "bottom",
+            "id": "b763caac-37c1-4659-ae0e-c7b7c760fec1"
+          },
+          {
+            "group": "left",
+            "id": "2408c946-a670-48b5-a676-d882946b8e36"
+          }
+        ]
+      },
+      "isCustom": true,
+      "parentKey": "1",
+      "x": 173,
+      "y": -170,
+      "zIndex": 10
+    }
+  ],
+  "edges": [
     {
       "id": "975bf288:a61170c3-8c1f18d0:89c0bc16",
-      "source": { "cell": "975bf288", "port": "a61170c3" },
-      "target": { "cell": "8c1f18d0", "port": "89c0bc16" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "975bf288",
+        "port": "a61170c3"
+      },
+      "target": {
+        "cell": "8c1f18d0",
+        "port": "89c0bc16"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "8c1f18d0:a3adcac9-41561012:293a90b5",
-      "source": { "cell": "8c1f18d0", "port": "a3adcac9" },
-      "target": { "cell": "41561012", "port": "293a90b5" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "8c1f18d0",
+        "port": "a3adcac9"
+      },
+      "target": {
+        "cell": "41561012",
+        "port": "293a90b5"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "5764f3ce:4b4d9fa6-1aed14d1:d348b56a",
-      "source": { "cell": "5764f3ce", "port": "4b4d9fa6" },
-      "target": { "cell": "1aed14d1", "port": "d348b56a" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": "5 5",
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "5764f3ce",
+        "port": "4b4d9fa6"
+      },
+      "target": {
+        "cell": "1aed14d1",
+        "port": "d348b56a"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":\"5 5\",\"strokeWidth\":1}}"
     },
     {
       "id": "1aed14d1:9a699e3f-4651130e:94f485b5",
-      "source": { "cell": "1aed14d1", "port": "9a699e3f" },
-      "target": { "cell": "4651130e", "port": "94f485b5" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
-    },
-    {
-      "id": "5764f3ce:ce88d7e2-3fb8d302:22d16375",
-      "source": { "cell": "5764f3ce", "port": "ce88d7e2" },
-      "target": { "cell": "3fb8d302", "port": "22d16375" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "1aed14d1",
+        "port": "9a699e3f"
+      },
+      "target": {
+        "cell": "4651130e",
+        "port": "94f485b5"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "3fb8d302:205f1437-a48131e0:bac7962b",
-      "source": { "cell": "3fb8d302", "port": "205f1437" },
-      "target": { "cell": "a48131e0", "port": "bac7962b" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "3fb8d302",
+        "port": "205f1437"
+      },
+      "target": {
+        "cell": "a48131e0",
+        "port": "bac7962b"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "a48131e0:4dee75d9-b57b57c8:73307680",
-      "source": { "cell": "a48131e0", "port": "4dee75d9" },
-      "target": { "cell": "b57b57c8", "port": "73307680" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": "5 5",
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "a48131e0",
+        "port": "4dee75d9"
+      },
+      "target": {
+        "cell": "b57b57c8",
+        "port": "73307680"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":\"5 5\",\"strokeWidth\":1}}"
     },
     {
       "id": "a48131e0:0f72f2ba-3631eae9:2aae2a71",
-      "source": { "cell": "a48131e0", "port": "0f72f2ba" },
-      "target": { "cell": "3631eae9", "port": "2aae2a71" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "a48131e0",
+        "port": "0f72f2ba"
+      },
+      "target": {
+        "cell": "3631eae9",
+        "port": "2aae2a71"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "3631eae9:8e0bff55-399bddb7:0bccd839",
-      "source": { "cell": "3631eae9", "port": "8e0bff55" },
-      "target": { "cell": "399bddb7", "port": "0bccd839" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "3631eae9",
+        "port": "8e0bff55"
+      },
+      "target": {
+        "cell": "399bddb7",
+        "port": "0bccd839"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "399bddb7:58f52f2c-c28a18d3:e67dc19c",
-      "source": { "cell": "399bddb7", "port": "58f52f2c" },
-      "target": { "cell": "c28a18d3", "port": "e67dc19c" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "399bddb7",
+        "port": "58f52f2c"
+      },
+      "target": {
+        "cell": "c28a18d3",
+        "port": "e67dc19c"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "c28a18d3:b578cc26-5359e23c:ff6724ee",
-      "source": { "cell": "c28a18d3", "port": "b578cc26" },
-      "target": { "cell": "5359e23c", "port": "ff6724ee" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "c28a18d3",
+        "port": "b578cc26"
+      },
+      "target": {
+        "cell": "5359e23c",
+        "port": "ff6724ee"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "5359e23c:34c9dbc6-5216c5dc:d2030f1b",
-      "source": { "cell": "5359e23c", "port": "34c9dbc6" },
-      "target": { "cell": "5216c5dc", "port": "d2030f1b" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "5359e23c",
+        "port": "34c9dbc6"
+      },
+      "target": {
+        "cell": "5216c5dc",
+        "port": "d2030f1b"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     },
     {
       "id": "b57b57c8:04c81e99-4651130e:1eb352b0",
-      "source": { "cell": "b57b57c8", "port": "04c81e99" },
-      "target": { "cell": "4651130e", "port": "1eb352b0" },
-      "attrs": {
-        "line": {
-          "stroke": "#A2B1C3",
-          "targetMarker": { "name": "block", "width": 12, "height": 8 },
-          "strokeDasharray": [0, 0],
-          "strokeWidth": 1
-        }
-      }
+      "source": {
+        "cell": "b57b57c8",
+        "port": "04c81e99"
+      },
+      "target": {
+        "cell": "4651130e",
+        "port": "1eb352b0"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "41561012:92334433-2427bf29:6bbf9ae4",
+      "source": {
+        "cell": "41561012",
+        "port": "92334433"
+      },
+      "target": {
+        "cell": "2427bf29",
+        "port": "6bbf9ae4"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "41561012:92334433-be25fe75:a705e7ed",
+      "source": {
+        "cell": "41561012",
+        "port": "92334433"
+      },
+      "target": {
+        "cell": "be25fe75",
+        "port": "a705e7ed"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "2427bf29:e1a1ecea-975bf288:15d1b217",
+      "source": {
+        "cell": "2427bf29",
+        "port": "e1a1ecea"
+      },
+      "target": {
+        "cell": "975bf288",
+        "port": "15d1b217"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "be25fe75:ce651308-975bf288:15d1b217",
+      "source": {
+        "cell": "be25fe75",
+        "port": "ce651308"
+      },
+      "target": {
+        "cell": "975bf288",
+        "port": "15d1b217"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "975bf288:821f59c0-5764f3ce:c29d7b43",
+      "source": {
+        "cell": "975bf288",
+        "port": "821f59c0"
+      },
+      "target": {
+        "cell": "5764f3ce",
+        "port": "c29d7b43"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "5764f3ce:ce88d7e2-node-186a9d31-0bd3-4b36-b61f-6b5380c824db:6d880b9d-7d7e-4357-94dd-caf7d73b5f80",
+      "source": {
+        "cell": "5764f3ce",
+        "port": "ce88d7e2"
+      },
+      "target": {
+        "cell": "node-186a9d31-0bd3-4b36-b61f-6b5380c824db",
+        "port": "6d880b9d-7d7e-4357-94dd-caf7d73b5f80"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "5764f3ce:ce88d7e2-node-c5171e2d-1cd8-4019-83dc-9f2ed0cab6e8:8637b869-7924-416a-b938-30a7ca932901",
+      "source": {
+        "cell": "5764f3ce",
+        "port": "ce88d7e2"
+      },
+      "target": {
+        "cell": "node-c5171e2d-1cd8-4019-83dc-9f2ed0cab6e8",
+        "port": "8637b869-7924-416a-b938-30a7ca932901"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "node-186a9d31-0bd3-4b36-b61f-6b5380c824db:e134d65d-a197-4116-ad74-f47dbbb727d1-node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862:2408c946-a670-48b5-a676-d882946b8e36",
+      "source": {
+        "cell": "node-186a9d31-0bd3-4b36-b61f-6b5380c824db",
+        "port": "e134d65d-a197-4116-ad74-f47dbbb727d1"
+      },
+      "target": {
+        "cell": "node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862",
+        "port": "2408c946-a670-48b5-a676-d882946b8e36"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "node-c5171e2d-1cd8-4019-83dc-9f2ed0cab6e8:6316d87e-2eb3-4ff6-bd8e-0cfd11bff4ce-node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862:2408c946-a670-48b5-a676-d882946b8e36",
+      "source": {
+        "cell": "node-c5171e2d-1cd8-4019-83dc-9f2ed0cab6e8",
+        "port": "6316d87e-2eb3-4ff6-bd8e-0cfd11bff4ce"
+      },
+      "target": {
+        "cell": "node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862",
+        "port": "2408c946-a670-48b5-a676-d882946b8e36"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
+    },
+    {
+      "id": "node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862:1ab6b2a4-dccd-41e9-83c6-d1642c9ad844-3fb8d302:22d16375",
+      "source": {
+        "cell": "node-2b3fc359-de49-4f9d-adb5-4eb70b6ba862",
+        "port": "1ab6b2a4-dccd-41e9-83c6-d1642c9ad844"
+      },
+      "target": {
+        "cell": "3fb8d302",
+        "port": "22d16375"
+      },
+      "attr": "{\"line\":{\"stroke\":\"#A2B1C3\",\"targetMarker\":{\"name\":\"block\",\"width\":12,\"height\":8},\"strokeDasharray\":[0,0],\"strokeWidth\":1}}"
     }
   ]
 }

+ 7 - 0
src/pages/PurchaseAdmin/PurchaseList/Flow/List.js

@@ -47,6 +47,13 @@ function List(props) {
     dispatch({
       type: 'flow/queryProject',
     });
+    // dispatch({
+    //   type: 'flow/getRoleList',
+    // });
+
+    // dispatch({
+    //   type: 'flow/queryDingTemplateList',
+    // });
   }, []);
 
   return (

+ 168 - 0
src/pages/PurchaseAdmin/PurchaseList/Flow/models/flow.js

@@ -6,20 +6,144 @@ import {
   queryBoomFlowDetail,
   updateNode,
   queryFlowList,
+  queryDingTemplateList,
+  saveAuditFlowInfo,
+  queryDepV2,
+  queryProcessFlows,
 } from '@/services/boom';
 import { queryRole } from '@/services/SysAdmin';
 import { queryProject } from '@/services/PurchaseList';
 import { message } from 'antd';
 
+function getDepUserTree(data) {
+  data.title = `${data.Name}`;
+  data.id = data.ID;
+  data.value = data.ID;
+  // data.selectable = false;
+  if (!data.children) data.children = new Array();
+
+  if (data.children) {
+    data.children.forEach(item => {
+      getDepUserTree(item);
+    });
+  }
+
+  if (data.Users && data.Users.length !== 0) {
+    data.Users.forEach(item => {
+      item.title = item.CName;
+      item.id = item.ID + '||' + data.ID;
+      item.value = item.ID + '||' + data.ID;
+      // item.selectable = true;
+      item.DepId = data.ID;
+      data.children.push(item);
+    });
+  }
+  return data;
+}
+
+const getFlowDetail = data => {
+  const groups = {
+    top: {
+      position: { name: 'top' },
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#31d0c6',
+          strokeWidth: 2,
+          fill: '#fff',
+          style: { visibility: 'hidden' },
+        },
+      },
+      zIndex: 10,
+    },
+    right: {
+      position: { name: 'right' },
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#31d0c6',
+          strokeWidth: 2,
+          fill: '#fff',
+          style: { visibility: 'hidden' },
+        },
+      },
+      zIndex: 10,
+    },
+    bottom: {
+      position: { name: 'bottom' },
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#31d0c6',
+          strokeWidth: 2,
+          fill: '#fff',
+          style: { visibility: 'hidden' },
+        },
+      },
+      zIndex: 10,
+    },
+    left: {
+      position: { name: 'left' },
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#31d0c6',
+          strokeWidth: 2,
+          fill: '#fff',
+          style: { visibility: 'hidden' },
+        },
+      },
+      zIndex: 10,
+    },
+  };
+  const attrs = {
+    line: {
+      stroke: '#A2B1C3',
+      targetMarker: { name: 'block', width: 12, height: 8 },
+      strokeDasharray: '5 5',
+      strokeWidth: 1,
+    },
+  };
+  let nodes = data.nodes.map(item => {
+    let node = { ...item };
+    node.ports.groups = groups;
+    node.parentKey = '1';
+
+    return node;
+  });
+  let edges = data.edges.map(item => {
+    let edge = { ...item };
+    try {
+      edge.attrs = item.attr ? JSON.parse(item.attr) : attrs;
+    } catch (error) {
+      edge.attrs = attrs;
+    }
+    return edge;
+  });
+  return {
+    ...data,
+    nodes,
+    edges,
+  };
+};
+
 export default {
   namespace: 'flow',
   state: {
     flowDetail: { nodes: [], edges: [] },
+    formData: {},
     auditList: [],
     flowList: [],
     projectList: [],
     current: {},
     roleList: [],
+    templateList: [],
+    depUserTree: [],
+    simpleFlowDteail: '',
   },
 
   effects: {
@@ -100,6 +224,50 @@ export default {
         callback && callback();
       }
     },
+    *queryDingTemplateList({ payload }, { call, put }) {
+      const response = yield call(queryDingTemplateList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { templateList: response.data.result },
+        });
+      }
+    },
+    *saveAuditFlowInfo({ payload, callback }, { call, put }) {
+      const response = yield call(saveAuditFlowInfo, payload);
+      if (response) {
+        message.success('保存成功');
+        callback && callback();
+      }
+    },
+    *fetchDepV2({ payload, callback }, { call, put }) {
+      const response = yield call(queryDepV2, { pageSize: 999999 });
+      if (response) {
+        // const depUserTree = response.data?.list;
+        const depUserTree = response.data.list.map(item => {
+          return getDepUserTree(item);
+        });
+        yield put({
+          type: 'save',
+          payload: { depUserTree },
+        });
+      }
+    },
+    *queryProcessFlows({ payload }, { call, put }) {
+      const data = yield call(queryProcessFlows, payload);
+      if (data && data.length > 0) {
+        yield put({
+          type: 'save',
+          payload: {
+            flowDetail: data[0].process_json
+              ? getFlowDetail(JSON.parse(data[0].process_json))
+              : { nodes: [], edges: [] },
+            formData: data[0].form_json ? JSON.parse(data[0].form_json) : [],
+            simpleFlowDteail: data[0].process_simple_json,
+          },
+        });
+      }
+    },
   },
 
   reducers: {

+ 32 - 18
src/pages/PurchaseAdmin/PurchaseList/Index.js

@@ -25,6 +25,20 @@ function LayoutDetail(props) {
       type: 'user/fetchCurrent',
     });
   }, []);
+  const checkReport = state => {
+    if (isAdmin) return true;
+    const manager = currentUser.is_leader || currentUser.is_opt_mgr || currentUser.is_wty_mgr;
+    switch (state) {
+      case 0:
+        return currentUser.is_accountant || manager || permission['func-01-point-works-report'];
+      case 1:
+        return manager || permission['func-01-point-works-report-p'];
+      case 2:
+        return permission['func-01-point-works-report-d'];
+      case 3:
+        return currentUser.is_accountant || permission['func-01-point-works-report-p-s'];
+    }
+  };
   return (
     <Layout>
       <Header>
@@ -37,7 +51,7 @@ function LayoutDetail(props) {
               defaultSelectedKeys={[props.location.pathname]}
               style={{ lineHeight: '64px', width: '100%' }}
             >
-              {/* <SubMenu key="/home/work-hours" title="工时管理">
+              <SubMenu key="/home/work-hours" title="工时管理">
                 <Menu.Item key="/home/work-hours">
                   <Link to="/home/work-hours">上报工时</Link>
                 </Menu.Item>
@@ -54,33 +68,33 @@ function LayoutDetail(props) {
                   <Link to="/home/approval/auth">审核列表</Link>
                 </Menu.Item>
               </SubMenu>
-      
-              {(isAdmin ||
-                currentUser.is_leader ||
-                currentUser.is_accountant ||
-                permission['func-01-point-works-report']) && (
+
+              {checkReport(0) && (
                 <SubMenu key="/home/report" title="工时报表">
-                  {(isAdmin ||
-                    currentUser.is_leader ||
-                    currentUser.is_accountant ||
-                    permission['func-01-point-works-report-p'] ||
-                    permission['func-01-point-works-report-p-s']) && (
+                  {/* <Menu.Item key="/home/report/resource">
+                  <Link to="/home/report/resource">资源报表</Link>
+                </Menu.Item> */}
+                  {checkReport(1) && (
                     <Menu.Item key="/home/report/project">
                       <Link to="/home/report/project">项目报表</Link>
                     </Menu.Item>
                   )}
-                  {(isAdmin || permission['func-01-point-works-report-d']) && (
+                  {checkReport(2) && (
                     <Menu.Item key="/home/report/department">
                       <Link to="/home/report/department">部门报表</Link>
                     </Menu.Item>
                   )}
+                  {checkReport(3) && (
+                    <Menu.Item key="/home/report/finance">
+                      <Link to="/home/report/finance">财务报表</Link>
+                    </Menu.Item>
+                  )}
                 </SubMenu>
-              )} */}
-              {/* {isAdmin && ( */}
-                <Menu.Item key="/home">
-                  <Link to="/home">采购清单</Link>
-                </Menu.Item>
-              {/* )} */}
+              )}
+
+              <Menu.Item key="/home">
+                <Link to="/home">采购清单</Link>
+              </Menu.Item>
               <Menu.Item key="/home/flow-list">
                 <Link to="/home/flow-list">流程图</Link>
               </Menu.Item>

+ 104 - 6
src/pages/PurchaseAdmin/PurchaseList/List/NewList.js

@@ -1,10 +1,22 @@
 import React, { useState, useEffect } from 'react';
-import { Table, Divider } from 'antd';
+import { Table, Divider, message } from 'antd';
 import { connect } from 'dva';
 import router from 'umi/router';
+import FlowModal from '../Detail/FlowModal';
+import { queryBoomFlowDetail, queryRecordSheet } from '@/services/boom';
+import { async } from '@antv/x6/es/registry/marker/async';
+import { getToken } from '@/utils/utils';
+import AuditForm from '@/components/AuditForm';
 
 function List(props) {
-  const { excel, loading, project, dispatch } = props;
+  const { excel, loading, project, dispatch, versionList } = props;
+  const [flowVisible, setFlowVisible] = useState(false);
+  const [version, setVersion] = useState({});
+  const [versionVisible, setVersionVisible] = useState(false);
+  const [flowDetail, setFlowDetail] = useState();
+  const [loading2, setLoading2] = useState(false);
+
+  let token = getToken();
 
   const columns = [
     {
@@ -26,9 +38,18 @@ function List(props) {
         if (record.is_parent) return null;
         return (
           <a
-            onClick={() => {
+            onClick={async () => {
               localStorage.excelId = record.id;
-              router.push(`/home/detail/${record.project_id}/${record.template_id}`);
+
+              setLoading2(true);
+              try {
+                const data = await queryBoomFlowDetail({ id: record.template_id });
+                setFlowDetail(data);
+                setVersion(record);
+                setFlowVisible(true);
+              } catch (error) {}
+              setLoading2(false);
+              // router.push(`/home/detail/${record.project_id}/${record.template_id}`);
             }}
           >
             查看
@@ -58,24 +79,101 @@ function List(props) {
     dispatch({
       type: 'newList/queryProject',
     });
+    dispatch({
+      type: 'newList/queryVersionsList',
+    });
+
+    dispatch({
+      type: 'user/queryDepV2',
+    });
   }, []);
 
+  const changeVersion = item => {
+    if (typeof item == 'object') {
+      localStorage.excelItem = JSON.stringify(item);
+    }
+    router.push(`/home/detail/${item.project_id}/${item.template_id}`);
+  };
+
+  const getLoading = () => {
+    let effects = loadingVersion.effects;
+    return !loadingVersion.effects['detail/queryComment'] && loadingVersion.models.detail;
+  };
+
+  const onCommit = async (values, id) => {
+    let currentNode = flowDetail.nodes.find?.(item => item.Id == version.template_node_id);
+    let sheets = await queryRecordSheet({ gridKey: version.id, 'JWT-TOKEN': token });
+    // if (!currentNode.muti_version) {
+    //   // audit_status=4 表示为清单推进后留存的副本.不计入多清单计算
+    //   let serviceVersion = versionList?.find(
+    //     item => item.audit_status != 4 && item.template_node_id == currentNode.Id
+    //   );
+    //   if (serviceVersion) {
+    //     message.error(`新建清单失败!业务节点【${currentNode.label}】只能有一个清单!`);
+    //     return;
+    //   }
+    // }
+    let params = {
+      ...values,
+      id: id,
+      project_id: version.project_id,
+      name: version.name,
+      guid: version.guid,
+      template_id: version.template_id,
+      template_node_id: version.template_node_id,
+      flow_id: version.flow_id,
+      node_id: version.node_id,
+      new_version: '0',
+      audit_status: 0,
+      data: sheets,
+      base_id: version.id,
+    };
+    dispatch({
+      type: 'newList/commitSheet',
+      payload: params,
+      callback: async newVersion => {
+        // 更新flow流程图
+        const data = await queryBoomFlowDetail({ id: newVersion.template_id });
+        console.log(data);
+        setFlowDetail(data);
+      },
+    });
+  };
+
   return (
     <div>
+      {/* <AuditForm onChange={values => console.log(values)} /> */}
       <Table
-        loading={loading}
+        loading={loading || loading2}
         rowKey="id"
         dataSource={excel.list}
         pagination={excel.pagination}
         columns={columns}
         onChange={queryList}
       />
+      <FlowModal
+        isOut={true}
+        flowDetail={flowDetail}
+        visible={flowVisible}
+        onClose={() => setFlowVisible(false)}
+        version={version}
+        onCommit={onCommit}
+        onChangeVersion={version => changeVersion(version)}
+      />
+      {/* <VersionModal
+        loading={getLoading()}
+        visible={versionVisible}
+        onClose={() => setVersionVisible(false)}
+        onOk={values => onCommit(values)}
+      /> */}
     </div>
   );
 }
 
-export default connect(({ newList, loading }) => ({
+export default connect(({ newList, loading, detail }) => ({
   excel: newList.excel,
   project: newList.project,
   loading: loading.models.newList,
+  loadingVersion: loading,
+  versionList: newList.versionList,
 }))(List);

+ 16 - 4
src/pages/PurchaseAdmin/PurchaseList/List/models/list.js

@@ -15,9 +15,8 @@ import {
   Logout,
   queryProject,
 } from '@/services/PurchaseList';
-import {
-  queryRole
-} from '@/services/SysAdmin';
+import { queryVersionsList } from '@/services/boom';
+import { queryRole } from '@/services/SysAdmin';
 import { setCurrentUser } from '@/utils/authority';
 import { storeToken } from '@/utils/utils';
 import { routerRedux } from 'dva/router';
@@ -47,6 +46,7 @@ export default {
     },
     comment: { list: [] },
     roleList: [],
+    versionList: [],
   },
 
   effects: {
@@ -117,7 +117,7 @@ export default {
       }
     },
     *queryHistory({ payload, callback }, { call, put }) {
-      payload.pageSize = '9999'
+      payload.pageSize = '9999';
       const response = yield call(queryHistory, payload);
       if (response) {
         yield put({
@@ -282,6 +282,18 @@ export default {
         });
       }
     },
+    *queryVersionsList({ payload, callback }, { call, put }) {
+      const response = yield call(queryVersionsList, payload);
+      if (response) {
+        callback && callback(response.data);
+        yield put({
+          type: 'save',
+          payload: {
+            versionList: response.data,
+          },
+        });
+      }
+    },
   },
 
   reducers: {

+ 46 - 3
src/pages/PurchaseAdmin/PurchaseList/List/models/newList.js

@@ -1,8 +1,7 @@
 import { queryProjectRecord } from '@/services/boom';
-import {
-  queryProject,
-} from '@/services/PurchaseList';
+import { queryProject } from '@/services/PurchaseList';
 import { message } from 'antd';
+import { commitSheet, queryVersionsList, queryAuditExcel, queryAuditRecord } from '@/services/boom';
 
 export default {
   namespace: 'newList',
@@ -15,6 +14,7 @@ export default {
       list: [],
       pagination: false,
     },
+    versionList: [],
   },
 
   effects: {
@@ -50,6 +50,49 @@ export default {
         });
       }
     },
+    *queryVersionsList({ payload, callback }, { call, put }) {
+      const response = yield call(queryVersionsList, payload);
+      if (response) {
+        callback && callback(response.data);
+        yield put({
+          type: 'save',
+          payload: {
+            versionList: response.data,
+          },
+        });
+      }
+    },
+    *commitSheet({ payload, callback }, { call, put }) {
+      let response = yield call(commitSheet, payload);
+      if (response) {
+        message.success('提交成功');
+        yield put({
+          type: 'queryVersionsList',
+          payload: {
+            project_id: payload.project_id,
+            // template_id: payload.template_id,
+            // template_node_id: payload.template_node_id,
+          },
+          callback: versionList => {
+            let newVersion = versionList.find(item => item.id == response.data.id);
+            callback && callback(newVersion);
+          },
+        });
+      }
+    },
+
+    *queryAuditExcel({ payload, callback }, { call, put }) {
+      const response = yield call(queryAuditExcel, payload);
+      if (response) {
+        callback && callback(response.data?.all);
+      }
+    },
+    *queryAuditRecord({ payload, callback }, { call, put }) {
+      const response = yield call(queryAuditRecord, payload);
+      if (response) {
+        callback && callback(response.data?.all);
+      }
+    },
   },
 
   reducers: {

+ 176 - 0
src/pages/PurchaseAdmin/PurchaseList/Report/DepCompareModal.js

@@ -0,0 +1,176 @@
+import React, { useState, useEffect } from 'react';
+import { Modal, Table } from 'antd';
+import { connect } from 'dva';
+import moment from 'moment';
+import { set } from 'lodash';
+
+function DepCompareModal(props) {
+  const {
+    dispatch,
+    visible,
+    onOk,
+    onCancel,
+    depCompare,
+    depUserProject,
+    depUser,
+    filter,
+    loading,
+  } = props;
+  const [userVisible, setUserVisible] = useState(false);
+  //本部门
+  const columns = [
+    { title: '用户名称', dataIndex: 'c_name' },
+    { title: '员工号', dataIndex: 'user_name' },
+    { title: '执行项目人日', dataIndex: 'type_project_cnt' },
+    { title: '售前支持', dataIndex: 'type_sale_cnt' },
+    { title: '市场品牌', dataIndex: 'type_market_cnt' },
+    { title: '日常', dataIndex: 'type_normal_cnt' },
+    { title: '标准化', dataIndex: 'type_standardize_cnt' },
+    { title: '研发', dataIndex: 'type_rd_cnt' },
+    { title: '漏填工时', dataIndex: 'type_lost_cnt' },
+    { title: '总计', dataIndex: 'total_cnt' },
+    {
+      title: '有效利用率',
+      dataIndex: 'usage_percent',
+      render: (percent = 0) => (percent * 100).toFixed(2) + '%',
+    },
+  ];
+  // 其他部门
+  const columnsDep = [
+    {
+      width: 350,
+      render: record =>
+        // record.dep_name || <a onClick={() => onClickUser(record)}>{record.c_name}</a>,
+        record.dep_name || record.c_name,
+    },
+    { title: '执行项目', dataIndex: 'type_project_cnt' },
+    { title: '售前项目', dataIndex: 'type_sale_cnt' },
+    { title: '研发项目', dataIndex: 'type_rd_cnt' },
+    { title: '运营项目', dataIndex: 'type_opt_cnt' },
+    { title: '质保项目', dataIndex: 'type_wty_cnt' },
+    { title: '总计', dataIndex: 'total_cnt' },
+  ];
+  const columnsUser = [
+    { title: '项目名称', dataIndex: 'project_name' },
+    { title: '总工时', dataIndex: 'total_cnt' },
+  ];
+
+  const onChangePage = pagination => {
+    dispatch({
+      type: 'report/queryUserReport',
+      payload: {
+        ...filter,
+        currentPage: pagination.current,
+      },
+    });
+  };
+
+  const onExpand = (expanded, record) => {
+    if (expanded && !record.isLoad) {
+      dispatch({
+        type: 'report/queryDepCompareUser',
+        payload: {
+          s_time: filter.s_time,
+          e_time: filter.e_time,
+          project_dep_id: filter.dep_id,
+          record: record,
+        },
+      });
+    }
+  };
+
+  const onClickUser = user => {
+    dispatch({
+      type: 'report/queryDepUserProject',
+      payload: {
+        s_time: filter.s_time,
+        e_time: filter.e_time,
+        project_dep_id: filter.dep_id,
+        dep_id: user.dep_id,
+        user_id: user.user_id,
+      },
+    });
+    setUserVisible(true);
+  };
+
+  useEffect(() => {
+    if (!filter?.dep_id) return;
+    dispatch({
+      type: 'report/queryUserReport',
+      payload: filter,
+    });
+    dispatch({
+      type: 'report/queryDepCompare',
+      payload: filter,
+    });
+  }, [filter]);
+
+  return (
+    <>
+      <Modal
+        title="部门对账单"
+        width="80%"
+        visible={visible}
+        onCancel={onCancel}
+        footer={false}
+        destroyOnClose
+      >
+        <Table
+          title={() => '本部门工时详情'}
+          columns={columns}
+          loading={loading}
+          dataSource={depUser.list}
+          pagination={depUser.pagination}
+          onChange={onChangePage}
+        ></Table>
+        {/* <Table
+          title={() => '本部门在所属项目下产生的工时'}
+          columns={columns}
+          loading={loading}
+          dataSource={depCompare.length == 0 ? [] : [depCompare[0]]}
+          rowKey="key"
+          childrenColumnName="child"
+          pagination={false}
+          onExpand={onExpand}
+        /> */}
+        {console.log(depCompare)}
+        {depCompare.length > 0 && (
+          <Table
+            title={() => '其他部门在所属项目下产生的工时'}
+            columns={columnsDep}
+            loading={loading}
+            dataSource={depCompare}
+            rowKey="key"
+            childrenColumnName="child"
+            pagination={false}
+            onExpand={onExpand}
+          />
+        )}
+      </Modal>
+
+      {/* <Modal
+        title="用户详情"
+        width="80%"
+        visible={userVisible}
+        onCancel={() => setUserVisible(false)}
+        footer={false}
+        destroyOnClose
+      >
+        <Table
+          columns={columnsUser}
+          loading={loading}
+          dataSource={depUserProject}
+          pagination={false}
+          rowKey="project_name"
+        ></Table>
+      </Modal> */}
+    </>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  depCompare: report.depCompare,
+  depUserProject: report.depUserProject,
+  depUser: report.depUser,
+  loading: loading.models.report,
+}))(DepCompareModal);

+ 31 - 41
src/pages/PurchaseAdmin/PurchaseList/Report/Department.js

@@ -1,23 +1,24 @@
 import React, { useEffect, useState, useRef } from 'react';
 import { connect } from 'dva';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Table, DatePicker, Input, Button } from 'antd';
+import { Form, Table, DatePicker, Input, Button } from 'antd';
 import styles from './report.less';
 import UserRptModal from './UserRptModal';
+import DepCompareModal from './DepCompareModal';
 import moment from 'moment';
-import { downloadFile, getToken } from '@/utils/utils.js'
+import { downloadFile, getToken } from '@/utils/utils.js';
 
 const { RangePicker } = DatePicker;
 
 function Department(props) {
-  const { dispatch, form, loading, dep } = props;
+  const { dispatch, loading, dep } = props;
+  const [form] = Form.useForm();
   const [visible, setVisible] = useState(false);
   const [modalFilter, setModalFilter] = useState({});
   const columns = [
     {
       title: '部门名称',
-      render: record => <a onClick={() => showUserModal(record)}>{record.dep_name}</a>,
+      // render: record => <a onClick={() => showUserModal(record)}>{record.dep_name}</a>,
+      render: record => <a onClick={() => showDepCompare(record)}>{record.dep_name}</a>,
     },
     {
       title: '执行项目人日',
@@ -67,46 +68,32 @@ function Department(props) {
     // },
   ];
   const filterRef = useRef({ pageSize: 99999 });
-  const onChangePage = pagination => {
+  const handleSearch = () => {
+    const { time } = form.getFieldsValue();
+    filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
+    filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
+
     dispatch({
-      type: 'report/queryUserReport',
+      type: 'report/queryDepReport',
       payload: {
-        ...filterRef.current,
-        currentPage: pagination.current,
+        filter: filterRef.current,
       },
     });
   };
-  const handleSearch = () => {
-    form.validateFields((error, { time }) => {
-      filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
-      filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
-
-      dispatch({
-        type: 'report/queryDepReport',
-        payload: {
-          filter: filterRef.current,
-        },
-      });
-    });
-  };
   const handleDownload = () => {
     const token = getToken();
-    const s_time = !(filterRef.current.s_time) ? '' : filterRef.current.s_time;
-    const e_time = !(filterRef.current.e_time) ? '' : filterRef.current.e_time;
-    downloadFile(`/api/v2/workload/rpt/dep/export2excel?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}`, `项目报表${moment().format('YYYYMMDDHHMMSS')}.xlsx`)
+    const s_time = !filterRef.current.s_time ? '' : filterRef.current.s_time;
+    const e_time = !filterRef.current.e_time ? '' : filterRef.current.e_time;
+    downloadFile(
+      `/api/v2/workload/rpt/dep/export2excel?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}`,
+      `项目报表${moment().format('YYYYMMDDHHMMSS')}.xlsx`
+    );
   };
   const renderSearch = () => {
-    const formItemLayout = {
-      labelCol: { span: 5 },
-      wrapperCol: { span: 18 },
-    };
-
     return (
-      <Form layout="inline" {...formItemLayout}>
-        <Form.Item label="时间">
-          {form.getFieldDecorator('time', {
-            initialValue: [moment().startOf('years'), moment().endOf('years')],
-          })(<RangePicker placeholder="选择时间" />)}
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={[moment().startOf('years'), moment()]}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
         </Form.Item>
         <Form.Item>
           <Button type="primary" loading={loading} onClick={handleSearch}>
@@ -127,7 +114,8 @@ function Department(props) {
       });
     }
   };
-  const showUserModal = item => {
+  // const showUserModal = item => {
+  const showDepCompare = item => {
     const { s_time, e_time } = filterRef.current;
     setModalFilter({
       s_time: s_time,
@@ -147,7 +135,9 @@ function Department(props) {
     <div>
       <div className={styles.topPart}>
         {renderSearch()}
-        <Button type="primary" onClick={handleDownload}>导出</Button>
+        <Button type="primary" onClick={handleDownload}>
+          导出
+        </Button>
       </div>
       <Table
         loading={loading}
@@ -156,10 +146,10 @@ function Department(props) {
         columns={columns}
         dataSource={dep.list}
         pagination={false}
-        onChange={onChangePage}
         onExpand={onExpand}
       />
-      <UserRptModal filter={modalFilter} visible={visible} onCancel={() => setVisible(false)} />
+      {/* <UserRptModal filter={modalFilter} visible={visible} onCancel={() => setVisible(false)} /> */}
+      <DepCompareModal filter={modalFilter} visible={visible} onCancel={() => setVisible(false)} />
     </div>
   );
 }
@@ -167,4 +157,4 @@ function Department(props) {
 export default connect(({ report, loading }) => ({
   dep: report.dep,
   loading: loading.models.report,
-}))(Form.create()(Department));
+}))(Department);

+ 122 - 0
src/pages/PurchaseAdmin/PurchaseList/Report/Finance.js

@@ -0,0 +1,122 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { connect } from 'dva';
+import { Form, Table, DatePicker, Input, Button, Select } from 'antd';
+import styles from './report.less';
+import moment from 'moment';
+import { downloadFile, getToken } from '@/utils/utils.js';
+
+const { RangePicker } = DatePicker;
+
+function Finance(props) {
+  const { dispatch, loading, finance } = props;
+  const [form] = Form.useForm();
+  const [modalFilter, setModalFilter] = useState({});
+  const columns = [
+    { title: '资源名称', dataIndex: 'c_name' },
+    { title: '资源编号', dataIndex: 'user_name' },
+    { title: '资源归属部门', dataIndex: 'dep_name' },
+    { title: '项目名称', dataIndex: 'project_name' },
+    { title: '项目编号', dataIndex: 'project_code' },
+    { title: '项目状态', dataIndex: 'project_status' },
+    { title: '项目归属部门', dataIndex: 'project_dep_name' },
+    { title: '工时类型', dataIndex: 'type_name' },
+    { title: '保存未提交', dataIndex: 'un_audit_cnt' },
+    { title: '提交未审批', dataIndex: 'pending_audit_cnt' },
+    { title: '已拒绝', dataIndex: 'refuse_audit_cnt' },
+    { title: '审批通过', dataIndex: 'pass_audit_cnt' },
+  ];
+  const STATUS = [
+    { value: 0, label: '售前' },
+    { value: 1, label: '转执行' },
+    { value: 2, label: '转运营' },
+    { value: 3, label: '转质保' },
+  ];
+  const filterRef = useRef({ pageSize: 20 });
+  const onChangePage = pagination => {
+    dispatch({
+      type: 'report/queryFinanceReport',
+      payload: {
+        ...filterRef.current,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  const handleSearch = () => {
+    const { time, project_name, project_status } = form.getFieldsValue();
+    filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
+    filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
+    filterRef.current.project_name = project_name;
+    filterRef.current.project_status = project_status;
+
+    dispatch({
+      type: 'report/queryFinanceReport',
+      payload: {
+        ...filterRef.current,
+      },
+    });
+  };
+  const handleDownload = () => {
+    const token = getToken();
+    const s_time = filterRef.current?.s_time || '';
+    const e_time = filterRef.current?.e_time || '';
+    const project_name = filterRef.current?.project_name || '';
+    const project_status = filterRef.current?.project_status || '';
+    downloadFile(
+      `/api/v2/workload/finance/export2excel?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}&project_name=${project_name}&project_status${project_status}`,
+      `财务报表${moment().format('YYYYMMDDHHMMSS')}.xlsx`
+    );
+  };
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={[moment().startOf('years'), moment()]}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item label="项目名称" name="project_name">
+          <Input />
+        </Form.Item>
+        <Form.Item label="项目状态" name="project_status" initialValue={null}>
+          <Select style={{ width: 120 }}>
+            <Select.Option value={null}>全部</Select.Option>
+            {STATUS.map(item => (
+              <Select.Option key={item.value}>{item.label}</Select.Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+  useEffect(() => {
+    handleSearch();
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.topPart}>
+        {renderSearch()}
+        <Button type="primary" onClick={handleDownload}>
+          导出
+        </Button>
+      </div>
+      <Table
+        loading={loading}
+        style={{ marginTop: 20 }}
+        rowKey={record => `${record.user_name}-${record.project_code}-${record.type_name}`}
+        columns={columns}
+        dataSource={finance.list}
+        pagination={finance.pagination}
+        onChange={onChangePage}
+      />
+    </div>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  finance: report.finance,
+  loading: loading.models.report,
+}))(Finance);

+ 137 - 112
src/pages/PurchaseAdmin/PurchaseList/Report/Project.js

@@ -1,8 +1,6 @@
 import React, { useEffect, useState, useRef } from 'react';
 import { connect } from 'dva';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Table, DatePicker, Input, Button, Select, message } from 'antd';
+import { Form, Table, DatePicker, Input, Button, Select, message, Popover } from 'antd';
 import report from './models/report';
 import styles from './report.less';
 import moment from 'moment';
@@ -11,7 +9,7 @@ import { downloadFile, getToken } from '@/utils/utils.js';
 
 const { Option } = Select;
 const { RangePicker } = DatePicker;
-const initDate = [moment().startOf('years'), moment().endOf('years')];
+const initDate = [moment().startOf('years'), moment()];
 
 // var currentYear = new Date().getFullYear();
 // var yearList = [];
@@ -21,20 +19,21 @@ const initDate = [moment().startOf('years'), moment().endOf('years')];
 // }
 
 function Resource(props) {
-  const { dispatch, form, loading, project } = props;
+  const { dispatch, loading, project } = props;
+  const [form] = Form.useForm();
   const [expandedRowKeys, setExpandedRowKeys] = useState([]);
   const [visible, setVisible] = useState(false);
   const [modalFilter, setModalFilter] = useState({});
   const filterRef = useRef({});
-  const onChangePage = pagination => {
-    dispatch({
-      type: 'report/queryProjectReport',
-      payload: {
-        ...filterRef.current,
-        currentPage: pagination.current,
-      },
-    });
-  };
+  // const onChangePage = pagination => {
+  //   dispatch({
+  //     type: 'report/queryProjectReport',
+  //     payload: {
+  //       ...filterRef.current,
+  //       currentPage: pagination.current,
+  //     },
+  //   });
+  // };
   const getMonthColumns = () => {
     let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
     var time;
@@ -63,65 +62,72 @@ function Resource(props) {
       date[cYear].push(arr[cMonth]);
       current.add('month', 1);
     }
-    console.log(date);
     current.subtract('month', 12);
     let monthColumns = Object.keys(date).map(year => ({
       title: year + '年',
-      children: date[year].map((item) => {
+      children: date[year].map(item => {
         let key = current.format('YYYY-MM');
         current.add('month', 1);
         return {
           title: `${item}`,
           render: record => {
             const { month } = record;
-            return month[key]?.total_audit_cnt || 0;
+            return month.find(item => item.st == key)?.pass_audit_cnt || 0;
+            // return month[key]?.total_audit_cnt || 0;
+            // return (
+            //   <Popover
+            //     content={
+            //       <>
+            //         {`审核通过: ${month[key]?.pass_audit_cnt || 0}`}
+            //         <br />
+            //         {`审核中: ${month[key]?.pending_audit_cnt || 0}`}
+            //         <br />
+            //         {`审核拒绝: ${month[key]?.refuse_audit_cnt || 0}`}
+            //         <br />
+            //         {`未提审: ${month[key]?.refuse_audit_cnt || 0}`}
+            //       </>
+            //     }
+            //   >
+            //     {month[key]?.total_audit_cnt || 0}
+            //   </Popover>
+            // );
             // return `${JSON.stringify(month)}`;
           },
         };
       }),
     }));
     return monthColumns;
-
-    // return {
-    //   title: '月份',
-    //   children: arr.map((item, index) => ({
-    //     title: `${item}`,
-    //     dataIndex: `month[${String(index)}].total_audit_cnt`,
-    //     render: cnt => cnt || 0,
-    //   })),
-    // };
-  };
-  const onExpand = (expanded, record) => {
-    if (expanded && !record.isLoad) {
-      dispatch({
-        type: 'report/queryProjectReportDetail',
-        payload: {
-          ...filterRef.current,
-          record: record,
-        },
-      });
-    }
   };
+  // const onExpand = (expanded, record) => {
+  //   if (expanded && !record.isLoad) {
+  //     dispatch({
+  //       type: 'report/queryProjectReportDetail',
+  //       payload: {
+  //         ...filterRef.current,
+  //         record: record,
+  //       },
+  //     });
+  //   }
+  // };
 
   const handleSearch = () => {
-    form.validateFields((error, { time, project_name }) => {
-      if (
-        !moment(time[0])
-          .add(12, 'month')
-          .isSameOrAfter(time[1])
-      ) {
-        message.error('时间间隔超过12个月,请重新选择。');
-        return;
-      }
-      filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
-      filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
-      dispatch({
-        type: 'report/queryProjectReport',
-        payload: {
-          ...filterRef.current,
-          project_name: project_name,
-        },
-      });
+    const { time, project_name } = form.getFieldsValue();
+    if (
+      !moment(time[0])
+        .add(12, 'month')
+        .isSameOrAfter(time[1])
+    ) {
+      message.error('时间间隔超过12个月,请重新选择。');
+      return;
+    }
+    filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
+    filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
+    dispatch({
+      type: 'report/queryProjectReport',
+      payload: {
+        ...filterRef.current,
+        project_name: project_name,
+      },
     });
   };
   const handleDownload = () => {
@@ -138,21 +144,12 @@ function Resource(props) {
   };
   const renderSearch = () => {
     return (
-      <Form layout="inline">
-        <Form.Item label="时间">
-          {form.getFieldDecorator('time', {
-            initialValue: initDate,
-          })(<RangePicker placeholder="选择时间" />)}
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
         </Form.Item>
-        <Form.Item label="项目">
-          {form.getFieldDecorator('project_name')(
-            <Input />
-            // <Select allowClear showSearch style={{ width: 240 }} placeholder="请选择项目" >
-            //   {projectList.map(item => (
-            //     <Option key={item.ID} value={item.Name}>{item.Name}</Option>
-            //   ))}
-            // </Select>
-          )}
+        <Form.Item label="项目" name="project_name">
+          <Input />
         </Form.Item>
         <Form.Item>
           <Button type="primary" loading={loading} onClick={handleSearch}>
@@ -166,61 +163,89 @@ function Resource(props) {
     {
       title: '项目名称',
       render: record =>
-        !record.isParent ? (
-          <a onClick={() => showUserModal(record)}>{record.type_name}</a>
+        !record.child && record.total_audit_cnt ? (
+          <a onClick={() => showUserModal(record)}>{record.name}</a>
         ) : (
-          record.type_name
+          record.name
         ),
     },
-    {
-      title: '分项工作代码',
-      align: 'center',
-      dataIndex: 'type_code',
-    },
-    // {
-    //   title: '项目名称',
-    //   render: record => (record.project_id == 0 ? '' : record.project_name),
-    // },
-    // {
-    //   title: '项目编号',
-    //   dataIndex: 'project_code',
-    // },
-    // {
-    //   title: '工作内容',
-    //   dataIndex: 'type_name',
-    // },
     ...getMonthColumns(),
-    {
-      title: '总计',
-      dataIndex: 'month_rpt',
-      render: arr => (arr ? arr.reduce((total, item) => total + item.total_audit_cnt, 0) : ''),
-    },
+    { title: '总计', dataIndex: 'total_audit_cnt' },
   ];
+  // const columns = [
+  //   {
+  //     title: '项目名称',
+  //     render: record =>
+  //       !record.isParent ? (
+  //         <a onClick={() => showUserModal(record)}>{record.type_name}</a>
+  //       ) : (
+  //         record.type_name
+  //       ),
+  //   },
+  //   {
+  //     title: '分项工作代码',
+  //     align: 'center',
+  //     dataIndex: 'type_code',
+  //   },
+  //   // {
+  //   //   title: '项目名称',
+  //   //   render: record => (record.project_id == 0 ? '' : record.project_name),
+  //   // },
+  //   // {
+  //   //   title: '项目编号',
+  //   //   dataIndex: 'project_code',
+  //   // },
+  //   // {
+  //   //   title: '工作内容',
+  //   //   dataIndex: 'type_name',
+  //   // },
+  //   ...getMonthColumns(),
+  //   {
+  //     title: '总计',
+  //     dataIndex: 'month_rpt',
+  //     render: arr => (
+  //       <Popover
+  //         content={
+  //           <>
+  //             {`审核通过: ${
+  //               arr ? arr.reduce((total, item) => total + item.pass_audit_cnt, 0) : ''
+  //             }`}
+  //             <br />
+  //             {`审核中: ${
+  //               arr ? arr.reduce((total, item) => total + item.pending_audit_cnt, 0) : ''
+  //             }`}
+  //             <br />
+  //             {`审核拒绝: ${
+  //               arr ? arr.reduce((total, item) => total + item.refuse_audit_cnt, 0) : ''
+  //             }`}
+  //             <br />
+  //             {`未提审: ${arr ? arr.reduce((total, item) => total + item.un_audit_cnt, 0) : ''}`}
+  //           </>
+  //         }
+  //       >
+  //         {arr ? arr.reduce((total, item) => total + item.total_audit_cnt, 0) : ''}
+  //       </Popover>
+  //     ),
+  //   },
+  // ];
   const showUserModal = item => {
     const { s_time, e_time } = filterRef.current;
     setModalFilter({
       s_time: s_time,
       e_time: e_time,
+      type_id: item.id,
       project_id: item.project_id,
-      p_type_id: item.project_id == 0 ? item.p_type_id : 0,
-      type_id: item.type_id,
+      flag: item.flag,
     });
     setVisible(true);
   };
   useEffect(() => {
-    // dispatch({
-    //   type: 'report/queryProjectReport',
-    // });
-    // dispatch({
-    //   type: 'report/queryProject',
-    // });
     handleSearch();
   }, []);
 
-  useEffect(() => {
-    setExpandedRowKeys(project.list.map(item => item.id));
-  }, [project.list]);
-  console.log(project.list)
+  // useEffect(() => {
+  //   setExpandedRowKeys(project.list.map(item => item.id));
+  // }, [project.list]);
 
   return (
     <div className={styles.page}>
@@ -233,11 +258,11 @@ function Resource(props) {
       <Table
         style={{ marginTop: 20 }}
         columns={columns}
-        rowKey="id"
-        onExpand={onExpand}
-        dataSource={project.list}
-        pagination={project.pagination}
-        onChange={onChangePage}
+        rowKey="key"
+        childrenColumnName="child"
+        // onExpand={onExpand}
+        dataSource={project}
+        pagination={false}
       />
       <UserProjectRptModal
         filter={modalFilter}
@@ -252,4 +277,4 @@ export default connect(({ report, loading }) => ({
   project: report.project,
   // projectList: report.projectList,
   loading: loading.models.report,
-}))(Form.create()(Resource));
+}))(Resource);

+ 1 - 3
src/pages/PurchaseAdmin/PurchaseList/Report/Resource.js

@@ -1,8 +1,6 @@
 import React, { useEffect, useState, useRef } from 'react';
 import { connect } from 'dva';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Table, DatePicker, Input, Button } from 'antd';
+import { Form, Table, DatePicker, Input, Button } from 'antd';
 import report from './models/report';
 import moment from 'moment';
 

+ 55 - 21
src/pages/PurchaseAdmin/PurchaseList/Report/UserProjectRptModal.js

@@ -1,23 +1,55 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Modal, Table, Input, Button } from 'antd';
+import { Modal, Table, Form, Input, Button } from 'antd';
 import { connect } from 'dva';
 import moment from 'moment';
 
-function AddModal(props) {
-  const { dispatch, visible, onOk, onCancel, data, filter, loading, form } = props;
+function UserProjectRptModal(props) {
+  const { dispatch, visible, onOk, onCancel, data, filter, loading } = props;
+  const [form] = Form.useForm();
+  // const getMonthColumns = () => {
+  //   let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
+
+  //   return {
+  //     title: '月份',
+  //     children: arr.map((item, index) => ({
+  //       title: `${item}`,
+  //       dataIndex: `month[${String(index)}].total_audit_cnt`,
+  //       render: cnt => cnt || 0,
+  //     })),
+  //   };
+  // };
   const getMonthColumns = () => {
     let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
+    var time = [moment(filter.s_time), moment(filter.e_time)];
+    var date = {};
+    let eYear = time[1].year();
+    let eMonth = time[1].month();
+    let current = moment(time[0]);
+    let cYear, cMonth;
 
-    return {
-      title: '月份',
-      children: arr.map((item, index) => ({
-        title: `${item}`,
-        dataIndex: `month[${String(index)}].total_audit_cnt`,
-        render: cnt => cnt || 0,
-      })),
-    };
+    for (let i = 0; i < 12; i++) {
+      cYear = current.year();
+      cMonth = current.month();
+      if (!date[cYear]) date[cYear] = [];
+      date[cYear].push(arr[cMonth]);
+      current.add('month', 1);
+    }
+    current.subtract('month', 12);
+    let monthColumns = Object.keys(date).map(year => ({
+      title: year + '年',
+      children: date[year].map(item => {
+        let key = current.format('YYYY-MM');
+        current.add('month', 1);
+        return {
+          title: `${item}`,
+          render: record => {
+            const { month_rpt } = record;
+            return month_rpt.find(item => item.ts == key)?.pass_audit_cnt || 0;
+          },
+        };
+      }),
+    }));
+    return monthColumns;
   };
   const columns = [
     {
@@ -28,11 +60,12 @@ function AddModal(props) {
       title: '部门',
       dataIndex: 'dep_name',
     },
-    getMonthColumns(),
+    // getMonthColumns(),
+    ...getMonthColumns(),
     {
       title: '总计',
       dataIndex: 'month_rpt',
-      render: arr => (arr ? arr.reduce((total, item) => total + item.total_audit_cnt, 0) : ''),
+      render: arr => (arr ? arr.reduce((total, item) => total + item.pass_audit_cnt, 0) : ''),
     },
   ];
   const onChangePage = pagination => {
@@ -45,7 +78,8 @@ function AddModal(props) {
     });
   };
   const handleSearch = () => {
-    if (!filter?.project_id && !filter?.p_type_id) return;
+    // if (!filter?.project_id && !filter?.p_type_id) return;
+    if (!filter?.classification) return;
     const dep_name = form.getFieldValue('dep_name');
     dispatch({
       type: 'report/queryUserProjectReport',
@@ -56,7 +90,7 @@ function AddModal(props) {
     });
   };
   useEffect(() => {
-    if (!filter?.project_id && !filter?.p_type_id) return;
+    if (!filter?.project_id) return;
     dispatch({
       type: 'report/queryUserProjectReport',
       payload: filter,
@@ -65,9 +99,9 @@ function AddModal(props) {
 
   return (
     <Modal title="工时" width="80%" visible={visible} onCancel={onCancel} footer={false}>
-      <Form layout="inline" style={{ marginBottom: 20 }}>
-        <Form.Item lable="部门">
-          {form.getFieldDecorator('dep_name')(<Input placeholder="请输入部门" />)}
+      <Form layout="inline" style={{ marginBottom: 20 }} form={form}>
+        <Form.Item lable="部门" name="dep_name">
+          <Input placeholder="请输入部门" />
         </Form.Item>
         <Form.Item>
           <Button type="primary" onClick={handleSearch}>
@@ -89,4 +123,4 @@ function AddModal(props) {
 export default connect(({ report, loading }) => ({
   data: report.projectUser,
   loading: loading.models.report,
-}))(Form.create()(AddModal));
+}))(UserProjectRptModal);

+ 2 - 2
src/pages/PurchaseAdmin/PurchaseList/Report/UserRptModal.js

@@ -3,7 +3,7 @@ import { Modal, Table } from 'antd';
 import { connect } from 'dva';
 import moment from 'moment';
 
-function AddModal(props) {
+function UserRptModal(props) {
   const { dispatch, visible, onOk, onCancel, data, filter, loading } = props;
   const columns = [
     {
@@ -94,4 +94,4 @@ function AddModal(props) {
 export default connect(({ report, loading }) => ({
   data: report.depUser,
   loading: loading.models.report,
-}))(AddModal);
+}))(UserRptModal);

+ 149 - 54
src/pages/PurchaseAdmin/PurchaseList/Report/models/report.js

@@ -6,11 +6,15 @@ import {
   queryProjectReportDetail,
   queryUserProjectReport,
   queryProject,
+  queryDepCompare,
+  queryDepCompareUser,
+  queryFinanceReport,
+  queryUserProject,
 } from '@/services/workHours';
 import { queryRole } from '@/services/SysAdmin';
 import { message } from 'antd';
 import moment from 'moment';
-import { filter } from 'lodash';
+import { filter, times } from 'lodash';
 import list from '../../List/models/list';
 
 export default {
@@ -32,10 +36,17 @@ export default {
       list: [],
       pagination: {},
     },
-    project: {
+    // project: {
+    //   list: [],
+    //   pagination: {},
+    // },
+    project: [],
+    finance: {
       list: [],
       pagination: {},
     },
+    depCompare: [],
+    depUserProject: [],
     projectList: [],
   },
 
@@ -63,18 +74,18 @@ export default {
     },
     *queryUserProjectReport({ payload }, { call, put }) {
       const { data } = yield call(queryUserProjectReport, payload);
-      try {
-        data.list.forEach(item => {
-          let month = {};
-          item.month_rpt.forEach(mItem => {
-            let m = moment(mItem.ts).month();
-            month[m] = mItem;
-          });
-          item.month = month;
-        });
-      } catch (error) {
-        console.error(error);
-      }
+      // try {
+      //   data.list.forEach(item => {
+      //     let month = {};
+      //     item.month_rpt.forEach(mItem => {
+      //       let m = moment(mItem.ts).month();
+      //       month[m] = mItem;
+      //     });
+      //     item.month = month;
+      //   });
+      // } catch (error) {
+      //   console.error(error);
+      // }
       yield put({
         type: 'save',
         payload: { projectUser: data },
@@ -84,7 +95,7 @@ export default {
       const { filter = {}, record = {} } = payload;
       const { data } = yield call(queryDepReport, { ...filter, dep_id: record.dep_id });
       data.list.forEach(item => {
-        if(item.sub_dep_num) item.children = [];
+        if (item.sub_dep_num) item.children = [];
         item.isLoad = false;
       });
       // 判断是否为根节点
@@ -94,7 +105,7 @@ export default {
         record.isLoad = true;
         yield put({
           type: 'save',
-          payload: { dep: { ...dep } },
+          payload: { dep: { ...dep, list: [...dep.list] } },
         });
       } else {
         yield put({
@@ -106,55 +117,88 @@ export default {
     *queryProjectReport({ payload }, { call, put }) {
       const { data } = yield call(queryProjectReport, {
         ...payload,
-        // pageSize: 20,
       });
-      let treeData = {};
-      try {
-        data.list.forEach(item => {
-          let p_type_id = item.project_id == 0 ? 't' + item.p_type_id : 'p' + item.project_id;
-          treeData[p_type_id] = {
-            id: p_type_id,
-            type_name:
-              item.project_id == 0
-                ? item.project_name
-                : `${item.project_name}(${item.project_code})`,
-            project_name: item.project_name,
-            project_id: item.project_id,
-            project_code: item.project_code,
-            p_type_id: item.p_type_id,
-            month_rpt: item.month_rpt,
-            type_code: '',
-            children: [],
-            month: {},
-            isLoad: false,
-            isParent: true,
-          };
-          item.month_rpt.forEach(mItem => {
-            // let m = moment(mItem.ts).month();
-            treeData[p_type_id].month[mItem.ts] = mItem;
-          });
-        });
-      } catch (error) {
-        console.log(error);
-      }
-      console.log(treeData);
+      const idHelper = (item, params) => {
+        if (params.flag === undefined) {
+          item.child.forEach(childItem => idHelper(childItem, { flag: item.id }));
+          item.key = 'f' + item.id;
+        } else {
+          item.flag = params.flag;
+          if (params.project_id === undefined) {
+            item.child?.forEach(childItem =>
+              idHelper(childItem, { flag: item.flag, project_id: item.id })
+            );
+            item.key = 'f' + item.flag + 'p' + item.id;
+          } else {
+            item.project_id = params.project_id;
+            item.key = 'f' + item.flag + 'p' + item.project_id + 't' + item.id;
+          }
+        }
+      };
+      data.forEach(item => idHelper(item, {}));
       yield put({
         type: 'save',
         payload: {
-          project: {
-            list: Object.values(treeData),
-            pagination: data.pagination,
-          },
+          project: data,
         },
       });
     },
+    // *queryProjectReport({ payload }, { call, put }) {
+    //   const { data } = yield call(queryProjectReport, {
+    //     ...payload,
+    //     // pageSize: 20,
+    //   });
+    //   let treeData = {};
+    //   try {
+    //     data.list.forEach(item => {
+    //       let p_type_id =
+    //         item.rpt.project_id == 0 ? 't' + item.rpt.p_type_id : 'p' + item.rpt.project_id;
+    //       treeData[p_type_id] = {
+    //         id: p_type_id,
+    //         type_name:
+    //           item.rpt.project_id == 0
+    //             ? item.rpt.project_name
+    //             : `${item.rpt.project_name}(${item.rpt.project_code})`,
+    //         project_name: item.rpt.project_name,
+    //         project_id: item.rpt.project_id,
+    //         project_code: item.rpt.project_code,
+    //         p_type_id: item.rpt.p_type_id,
+    //         month_rpt: item.rpt.month_rpt,
+    //         classification: item.classification,
+    //         project_ids: item.project_ids,
+    //         type_code: '',
+    //         children: [],
+    //         month: {},
+    //         isLoad: false,
+    //         isParent: true,
+    //       };
+    //       item.rpt.month_rpt.forEach(mItem => {
+    //         // let m = moment(mItem.ts).month();
+    //         treeData[p_type_id].month[mItem.ts] = mItem;
+    //       });
+    //     });
+    //   } catch (error) {
+    //     console.log(error);
+    //   }
+    //   console.log(treeData);
+    //   yield put({
+    //     type: 'save',
+    //     payload: {
+    //       project: {
+    //         list: Object.values(treeData),
+    //         pagination: data.pagination,
+    //       },
+    //     },
+    //   });
+    // },
     *queryProjectReportDetail({ payload = {} }, { call, put, select }) {
       const { record = {} } = payload;
       const { data } = yield call(queryProjectReportDetail, {
         s_time: payload.s_time,
         e_time: payload.e_time,
-        project_id: record.project_id,
+        classification: record.classification,
         p_type_id: record.p_type_id,
+        project_ids: record.project_ids.join(','),
       });
       data.forEach((item, index) => {
         let month = {};
@@ -162,18 +206,69 @@ export default {
           // let m = moment(mItem.ts).month();
           month[mItem.ts] = mItem;
         });
-      
+
         item.month = month;
         item.id = record.id + '_' + index;
       });
       const project = yield select(s => s.report.project);
-      record.children = data.length == 0 ? null : data;
+      record.children =
+        data.length == 0 ? null : data.map(item => ({ ...item, project_ids: record.project_ids }));
       record.isLoad = true;
       yield put({
         type: 'save',
         payload: { project: { ...project } },
       });
     },
+    *queryFinanceReport({ payload = {} }, { call, put }) {
+      const { data } = yield call(queryFinanceReport, payload);
+      yield put({
+        type: 'save',
+        payload: { finance: data },
+      });
+    },
+    *queryDepCompare({ payload }, { call, put }) {
+      let { data } = yield call(queryDepCompare, payload);
+      if (data) {
+        const treeHelper = item => {
+          item.key = 'd' + item.dep_id;
+          if (!item.child && item.total_cnt) item.child = [];
+          item.child && item.child.forEach(childItem => treeHelper(childItem));
+        };
+        data.forEach(item => treeHelper(item));
+      } else data = [];
+      yield put({
+        type: 'save',
+        payload: { depCompare: data },
+      });
+    },
+    *queryDepCompareUser({ payload }, { call, put, select }) {
+      try {
+        let record = payload.record;
+        payload.dep_id = record.dep_id;
+        delete payload.record;
+        let { data } = yield call(queryDepCompareUser, payload);
+        data.forEach(item => {
+          item.key = 'u' + item.user_id;
+          item.dep_id = payload.dep_id;
+        });
+        record.child = [...record.child, ...data];
+        record.isLoad = true;
+      } catch (error) {
+        console.log(error);
+      }
+      const depCompare = yield select(s => s.report.depCompare);
+      yield put({
+        type: 'save',
+        payload: { depCompare: [...depCompare] },
+      });
+    },
+    *queryDepUserProject({ payload }, { call, put }) {
+      const { data } = yield call(queryUserProject, payload);
+      yield put({
+        type: 'save',
+        payload: { depUserProject: data },
+      });
+    },
   },
 
   reducers: {

+ 101 - 94
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/AddModal.js

@@ -1,19 +1,28 @@
 import React, { useState } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { message, Modal, Select, DatePicker } from 'antd';
+import { message, Form, Modal, Select, DatePicker } from 'antd';
+import { connect } from 'dva';
 import moment from 'moment';
 
 const { Option } = Select;
 const { MonthPicker } = DatePicker;
 
 function AddModal(props) {
-  const { visible, onOk, onCancel, time, form, typeList = [], projectList = {}, loading } = props;
+  const {
+    visible,
+    onOk,
+    onCancel,
+    time,
+    typeList = [],
+    subTypeList = [],
+    dispatch,
+    project = [],
+    loading,
+  } = props;
+  const [form] = Form.useForm();
   const [type, setType] = useState({});
   const [subType, setSubType] = useState({});
   const handleOk = () => {
-    form.validateFields((error, values) => {
-      if (error) return;
+    form.validateFields().then(values => {
       if (type.type == 0 || subType.type == 0) {
         if (!values.project) {
           message.error('请选择项目');
@@ -50,56 +59,75 @@ function AddModal(props) {
   };
   const onChangeType = value => {
     let item = typeList.find(t => t.id == value);
-    if (value == 35) {
-      form.setFieldsValue({
-        project: null,
-        subType: item.children[0].id + '',
+    form.setFieldsValue({
+      project: null,
+      subType: null,
+    });
+    setType(item);
+    setSubType({});
+    dispatch({
+      type: 'workload/save',
+      payload: { subTypeList: [] },
+    });
+    if (!item.type) {
+      dispatch({
+        type: 'workload/queryProject',
+        payload: { stage: value },
       });
-      setSubType(item.children[0]);
-      setType(item);
     } else {
-      form.setFieldsValue({
-        project: null,
-        subType: null,
+      dispatch({
+        type: 'workload/querySubType',
+        payload: { parent_id: item.id },
+        callback: subTypeList => {
+          if (value == 33) {
+            form.setFieldsValue({
+              project: null,
+              subType: subTypeList[0].id + '',
+            });
+            setSubType(subTypeList[0]);
+          }
+        },
       });
-      setSubType({});
-      setType(item);
     }
   };
   const onChangeSubType = value => {
-    let item = type.children.find(t => t.id == value);
+    let item = subTypeList.find(t => t.id == value);
     setSubType(item);
   };
 
   //售前支持更改项目时与子类选单联动
   const onChangeProject = value => {
-    if (value == '0') {
-      form.setFieldsValue({ subType: type.children[0].id + '' });
-      setSubType(type.children[0]);
-    } else {
-      form.setFieldsValue({ subType: type.children[1].id + '' });
-      setSubType(type.children[1]);
-    }
+    form.setFieldsValue({ subType: '' });
+    dispatch({
+      type: 'workload/querySubType',
+      payload: { parent_id: type.id, project_id: value },
+      callback: subTypeList => {
+        if (type.id == 35) {
+          form.setFieldsValue({ subType: subTypeList[0].id + '' });
+          setSubType(subTypeList[0]);
+        }
+        if (type.id == 2) {
+          if (value == '0') {
+            form.setFieldsValue({ subType: subTypeList[0].id + '' });
+            setSubType(subTypeList[0]);
+          } else {
+            form.setFieldsValue({ subType: subTypeList[1].id + '' });
+            setSubType(subTypeList[1]);
+          }
+        }
+      },
+    });
   };
 
   //分类选单
   const renderType = () => {
     return (
-      <Form.Item label="分类">
-        {form.getFieldDecorator('type', {
-          rules: [
-            {
-              required: true,
-              message: '请选择分类',
-            },
-          ],
-        })(
-          <Select onChange={onChangeType} placeholder="请选择分类">
-            {typeList.map(item => (
-              <Option key={String(item.id)}>{item.name}</Option>
-            ))}
-          </Select>
-        )}
+      <Form.Item label="分类" name="type" rules={[{ required: true, message: '请选择分类' }]}>
+        <Select onChange={onChangeType} placeholder="请选择分类">
+          {typeList.map(item => (
+            <Option key={String(item.id)}>{item.name}</Option>
+          ))}
+        </Select>
       </Form.Item>
     );
   };
@@ -107,37 +135,22 @@ function AddModal(props) {
   //项目选单
   const renderProject = () => {
     return (
-      <Form.Item label="项目">
-        {form.getFieldDecorator('project', {
-          rules: [
-            {
-              required: true,
-              message: '请选择项目',
-            },
-          ],
-        })(
-          //售前支持特殊判断,增加“无项目”,选择项目时与子类联动
-          <Select
-            showSearch
-            onChange={type.id === 2 ? onChangeProject : null}
-            placeholder="请选择项目"
-            optionFilterProp="children"
-            filterOption={(input, option) =>
-              // console.log(option.props.children)
-              option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-            }
-          >
-            {type.id === 2 && <Option key={'0'}>售前支持(10100)</Option>}
-            {(type.id === 2
-              ? projectList.project0
-              : type.id === 3
-              ? projectList.project1
-              : projectList.project7
-            ).map(item => (
-              <Option key={String(item.ID)}>{`${item.Name}(${item.Code})`}</Option>
-            ))}
-          </Select>
-        )}
+      <Form.Item label="项目" name="project" rules={[{ required: true, message: '请选择项目' }]}>
+        {/* 售前支持特殊判断,增加“无项目”,选择项目时与子类联动 */}
+        <Select
+          showSearch
+          onChange={onChangeProject}
+          placeholder="请选择项目"
+          optionFilterProp="children"
+          filterOption={(input, option) =>
+            option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+          }
+        >
+          {type.id === 2 && <Option key={'0'}>售前支持(10100)</Option>}
+          {project.map(item => (
+            <Option key={String(item.ID)}>{`${item.Name}(${item.Code})`}</Option>
+          ))}
+        </Select>
       </Form.Item>
     );
   };
@@ -145,26 +158,17 @@ function AddModal(props) {
   //子类选单
   const renderSubType = () => {
     return (
-      <Form.Item label="子类">
-        {form.getFieldDecorator('subType', {
-          rules: [
-            {
-              required: true,
-              message: '请选择子类',
-            },
-          ],
-        })(
-          //售前支持特殊判断,子类不可选
-          <Select
-            disabled={type.id === 2 || type.id === 35}
-            onChange={onChangeSubType}
-            placeholder={type.id === 2 || type.id === 35 ? null : '请选择子类'}
-          >
-            {(type.children || []).map(item => (
-              <Option key={String(item.id)}>{`${item.name}(${item.code})`}</Option>
-            ))}
-          </Select>
-        )}
+      <Form.Item label="子类" name="subType" rules={[{ required: true, message: '请选择子类' }]}>
+        {/* 售前支持特殊判断,子类不可选 */}
+        <Select
+          disabled={type.id === 2}
+          onChange={onChangeSubType}
+          placeholder={type.id === 2 ? null : '请选择子类'}
+        >
+          {(subTypeList || []).map(item => (
+            <Option key={String(item.id)}>{`${item.name}(${item.code})`}</Option>
+          ))}
+        </Select>
       </Form.Item>
     );
   };
@@ -179,9 +183,9 @@ function AddModal(props) {
       onOk={handleOk}
       destroyOnClose
     >
-      <Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }}>
+      <Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} form={form}>
         {renderType()}
-        {(type.id === 2 || type.id === 3 || type.id === 35) && renderProject()}
+        {type.type == 0 && renderProject()}
         {renderSubType()}
         <Form.Item label="时间">{time}</Form.Item>
       </Form>
@@ -189,4 +193,7 @@ function AddModal(props) {
   );
 }
 
-export default Form.create()(AddModal);
+export default connect(({ workload }) => ({
+  project: workload.project,
+  subTypeList: workload.subTypeList,
+}))(AddModal);

+ 59 - 14
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/Auth.js

@@ -1,6 +1,4 @@
 import React, { useState, useEffect, useRef } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
 import {
   message,
   InputNumber,
@@ -20,10 +18,9 @@ import AuthWorkList from './AuthWorkList';
 import { connect } from 'dva';
 import styles from './index.less';
 import moment from 'moment';
-import { getList } from '@/services/devOpsScoreRule';
 
 function List(props) {
-  const { typeList, dispatch, loading, form, list, project, currentUser, allType, user } = props;
+  const { dispatch, loading, list, project, currentUser, allType, user } = props;
   const [visible, setVisible] = useState(false);
   const [filter, setFilter] = useState({});
   const [current, setCurrent] = useState({
@@ -45,9 +42,16 @@ function List(props) {
           status: 2,
           desc: '',
         }));
+        let time = current.date.format('YYYY-MM-DD');
         dispatch({
           type: 'workload/authWorkload',
           payload: params,
+          callback: list => {
+            setCurrent({
+              ...current,
+              list: list.filter(item => item.time == time),
+            });
+          },
         });
       },
     });
@@ -62,11 +66,16 @@ function List(props) {
       status: 3,
       desc: data.desc || '',
     }));
+    let time = current.date.format('YYYY-MM-DD');
     dispatch({
       type: 'workload/authWorkload',
       payload: params,
-      callback: () => {
+      callback: list => {
         setVisible(false);
+        setCurrent({
+          ...current,
+          list: list.filter(item => item.time == time),
+        });
       },
     });
   };
@@ -84,10 +93,32 @@ function List(props) {
 
   const onChangeDate = value => {
     let time = value.format('YYYY-MM-DD');
-    setCurrent({
-      date: value,
-      list: list.filter(item => item.time == time),
-    });
+    if (current.date.format('YYYY-MM') != value.format('YYYY-MM')) {
+      const s_date = value.format('YYYY-MM') + '-01';
+      const e_date = moment(s_date)
+        .add('month', 1)
+        .add('days', -1)
+        .format('YYYY-MM-DD');
+      dispatch({
+        type: 'workload/queryAuthWorkHours',
+        payload: {
+          pageSize: 9999,
+          s_time: s_date + ' 00:00:00',
+          e_time: e_date + ' 23:59:59',
+        },
+        callback: list => {
+          setCurrent({
+            date: value,
+            list: list.filter(item => item.time == time),
+          });
+        },
+      });
+    } else {
+      setCurrent({
+        date: value,
+        list: list.filter(item => item.time == time),
+      });
+    }
   };
 
   const getList = () => {
@@ -105,14 +136,29 @@ function List(props) {
     dispatch({
       type: 'workload/queryWorkType',
       callback: () => {
+        const s_date = current.date.format('YYYY-MM') + '-01';
+        const e_date = moment(s_date)
+          .add('month', 1)
+          .add('days', -1)
+          .format('YYYY-MM-DD');
         dispatch({
           type: 'workload/queryAuthWorkHours',
           payload: {
             pageSize: 9999,
+            s_time: s_date + ' 00:00:00',
+            e_time: e_date + ' 23:59:59',
+          },
+          callback: list => {
+            let time = current.date.format('YYYY-MM-DD');
+            setCurrent({
+              ...current,
+              list: list.filter(item => item.time == time),
+            });
           },
         });
       },
     });
+
     // 查询项目列表
     dispatch({
       type: 'workload/queryProject',
@@ -132,9 +178,9 @@ function List(props) {
     };
   }, [currentUser.ID]);
 
-  useEffect(() => {
-    onChangeDate(current.date);
-  }, [list]);
+  // useEffect(() => {
+  //   onChangeDate(current.date);
+  // }, [list]);
 
   return (
     <div>
@@ -170,10 +216,9 @@ function List(props) {
 
 export default connect(({ workload, user, loading }) => ({
   list: workload.list,
-  typeList: workload.typeList,
   user: user.list,
   allType: workload.allType,
   project: workload.project,
   currentUser: user.currentUser,
   loading: loading.models.workload,
-}))(Form.create()(List));
+}))(List);

+ 32 - 52
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/AuthWorkList.js

@@ -1,7 +1,6 @@
 import React, { useState } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
 import {
+  Form,
   InputNumber,
   Input,
   Button,
@@ -20,18 +19,8 @@ const { Panel } = Collapse;
 const { Option } = Select;
 
 function AuthWorkList(props) {
-  const {
-    list,
-    onAuth,
-    onSearch,
-    onSave,
-    project = [],
-    allType,
-    onAgree,
-    onReject,
-    user,
-    form,
-  } = props;
+  const { list, onAuth, onSearch, onSave, project = [], allType, onAgree, onReject, user } = props;
+  const [form] = Form.useForm();
   const [edit, setEdit] = useState(false);
   const columns = [
     {
@@ -41,11 +30,7 @@ function AuthWorkList(props) {
     },
     {
       title: '提交人',
-      dataIndex: 'User.CName',
-      // render: (project_id, item) => {
-      //   if (item.zIndex === 0) return '';
-      //   return project.find(p => p.ID == project_id)?.Name || '-';
-      // },
+      dataIndex: ['User', 'CName'],
     },
     {
       title: '审核状态',
@@ -107,7 +92,7 @@ function AuthWorkList(props) {
       if (id == '0') {
         name = '其他';
       } else {
-        name = project.find(p => p.ID == id)?.Name;
+        name = item.Project.Name;
       }
 
       if (!data[id]) data[id] = { list: [], name, id };
@@ -155,46 +140,41 @@ function AuthWorkList(props) {
 
   const renderSearch = () => {
     const onHandleSearch = () => {
-      form.validateFields((error, values) => {
-        if (error) return;
+      form.validateFields().then(values => {
         onSearch(values);
       });
     };
     return (
-      <Form style={{ marginBottom: 20 }}>
+      <Form style={{ marginBottom: 20 }} form={form}>
         <Row gutter={12}>
           <Col span={12}>
-            <Form.Item label="人员">
-              {form.getFieldDecorator('userId')(
-                <Select
-                  allowClear
-                  showSearch
-                  filterOption={(input, option) =>
-                    option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-                  }
-                >
-                  {user.map(item => (
-                    <Option key={item.ID}>{item.CName}</Option>
-                  ))}
-                </Select>
-              )}
+            <Form.Item label="人员" name="userId">
+              <Select
+                allowClear
+                showSearch
+                filterOption={(input, option) =>
+                  option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }
+              >
+                {user.map(item => (
+                  <Option key={item.ID}>{item.CName}</Option>
+                ))}
+              </Select>
             </Form.Item>
           </Col>
           <Col span={12}>
-            <Form.Item label="项目">
-              {form.getFieldDecorator('projectId')(
-                <Select
-                  allowClear
-                  showSearch
-                  filterOption={(input, option) =>
-                    option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-                  }
-                >
-                  {project.map(item => (
-                    <Option key={item.ID}>{item.Name}</Option>
-                  ))}
-                </Select>
-              )}
+            <Form.Item label="项目" name="projectId">
+              <Select
+                allowClear
+                showSearch
+                filterOption={(input, option) =>
+                  option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                }
+              >
+                {project.map(item => (
+                  <Option key={item.ID}>{`${item.Name}(${item.Code})`}</Option>
+                ))}
+              </Select>
             </Form.Item>
           </Col>
         </Row>
@@ -243,4 +223,4 @@ function AuthWorkList(props) {
   );
 }
 
-export default Form.create()(AuthWorkList);
+export default AuthWorkList;

+ 13 - 26
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/CalendarModal.js

@@ -1,12 +1,11 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
 import {
   InputNumber,
   message,
   Modal,
   Calendar,
   Popover,
+  Form,
   Input,
   Row,
   Col,
@@ -17,18 +16,8 @@ import styles from './index.less';
 import moment from 'moment';
 
 function CalendarModal(props) {
-  const {
-    allType,
-    projectList,
-    visible,
-    onOk,
-    onCancel,
-    data,
-    title,
-    form,
-    footer,
-    loading,
-  } = props;
+  const { allType, projectList, visible, onOk, onCancel, data, title, footer, loading } = props;
+  const [form] = Form.useForm();
   const [type, setType] = useState({});
   const [edit, setEdit] = useState(false);
   const [validRange, setValidRange] = useState(null);
@@ -37,9 +26,7 @@ function CalendarModal(props) {
     list: [],
   });
   const handleOk = () => {
-    form.validateFields((error, values) => {
-      if (error) return;
-      console.log(values);
+    form.validateFields().then(values => {
       let params = [];
       Object.keys(values).forEach(type_id => {
         if (type_id == 'comment') return;
@@ -147,16 +134,16 @@ function CalendarModal(props) {
         <Col span={6}>
           <Button>添加</Button>
           {current.list.map(item => (
-            <Form.Item label={item.TypeInfo?.name}>
-              {form.getFieldDecorator(`${item.type_id}.${item.time}`, {
-                initialValue: item?.workload,
-              })(<InputNumber min={0} max={8} disabled={!edit} />)}
+            <Form.Item
+              label={item.TypeInfo?.name}
+              name={`${item.type_id}.${item.time}`}
+              initialValue={item?.workload}
+            >
+              <InputNumber min={0} max={8} disabled={!edit} />
             </Form.Item>
           ))}
-          <Form.Item label="日志">
-            {form.getFieldDecorator('comment', { initialValue: current.list[0]?.comment })(
-              <Input.TextArea rows={5} disabled={!edit} />
-            )}
+          <Form.Item label="日志" name="comment" initialValue={current.list[0]?.comment}>
+            <Input.TextArea rows={5} disabled={!edit} />
           </Form.Item>
 
           <div className={styles.btns}>
@@ -186,4 +173,4 @@ function CalendarModal(props) {
   );
 }
 
-export default Form.create()(CalendarModal);
+export default CalendarModal;

+ 9 - 10
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/RejectModal.js

@@ -1,15 +1,12 @@
 import React, { useState } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Input, message, Modal } from 'antd';
+import { Input, message, Form, Modal } from 'antd';
 
 function RejectModal(props) {
-  const { visible, onOk, onCancel, form, loading } = props;
+  const { visible, onOk, onCancel, loading } = props;
+  const [form] = Form.useForm();
   const [type, setType] = useState({});
   const handleOk = () => {
-    form.validateFields((error, values) => {
-      if (error) return;
-
+    form.validateFields().then(values => {
       onOk(values);
     });
   };
@@ -24,11 +21,13 @@ function RejectModal(props) {
       onOk={handleOk}
       destroyOnClose
     >
-      <Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }}>
-        <Form.Item label="拒绝理由">{form.getFieldDecorator('desc')(<Input.TextArea />)}</Form.Item>
+      <Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} form={form}>
+        <Form.Item label="拒绝理由" name="desc">
+          <Input.TextArea />
+        </Form.Item>
       </Form>
     </Modal>
   );
 }
 
-export default Form.create()(RejectModal);
+export default RejectModal;

+ 1 - 3
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/SearchForm.js

@@ -1,7 +1,5 @@
 import React from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Select, DatePicker, Button } from 'antd';
+import { Form, Select, DatePicker, Button } from 'antd';
 import moment from 'moment';
 
 const { Option } = Select;

+ 30 - 16
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/WorkList.js

@@ -1,60 +1,74 @@
 import React, { useState, useEffect } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
 import { InputNumber, Popover, Divider, Table, message } from 'antd';
 
 function WorkList(props) {
-  const { list, form, onAuth, onSave, project = [], allType } = props;
+  const { list, onAuth, onSave, allType } = props;
   const [expandedRowKeys, setExpandedRowKeys] = useState([]);
   const [dataSource, setDataSource] = useState([]);
+  let workHour = {};
   const columns = [
     {
       title: '分类',
       dataIndex: 'type_id',
-      width: 230,
+      width: '25%',
       render: type_id => allType[type_id]?.name,
     },
     {
       title: '所属项目',
-      dataIndex: 'project_id',
-      render: (project_id, item) => {
+      width: '30%',
+      render: item => {
         if (item.zIndex === 0) return '';
-        return project.find(p => p.ID == project_id)?.Name || '-';
+        return item.Project?.Name || '-';
       },
     },
     {
       title: '审核状态',
-      width: 100,
+      width: '15%',
       render: record => renderStatus(record),
     },
     {
       title: '工时',
-      width: 110,
+      width: '15%',
       render: item => {
         if (item.zIndex === 0) {
           return '总计 ' + item.children.reduce((total, item) => total + item.workload, 0) + '小时';
         }
         if (item.audit_state == 1 || item.audit_state == 2) return item?.workload + '小时';
-        return form.getFieldDecorator(`${item.type_id}.${item.time}`, {
-          initialValue: item?.workload,
-        })(<InputNumber style={{width: 60}} min={0} step={0.5} />);
+        workHour[`${item.type_id}.${item.project_id}.${item.time}`] = item?.workload;
+        return (
+          <InputNumber
+            style={{ width: 60 }}
+            min={0}
+            step={0.5}
+            defaultValue={item?.workload}
+            onChange={value => {
+              workHour[`${item.type_id}.${item.project_id}.${item.time}`] = value;
+            }}
+          />
+        );
       },
     },
     {
       title: '操作',
-      width: 140,
+      width: '15%',
       render: item => {
         if (item.zIndex === 0) return '';
         if (item.audit_state == 1 || item.audit_state == 2) return;
         return (
           <>
             <a
-              onClick={() => onHandleSave(item, form.getFieldValue(`${item.type_id}.${item.time}`))}
+              onClick={() =>
+                onHandleSave(item, workHour[`${item.type_id}.${item.project_id}.${item.time}`])
+              }
             >
               保存
             </a>
             <Divider type="vertical"></Divider>
-            <a onClick={() => onAuth(item, form.getFieldValue(`${item.type_id}.${item.time}`))}>
+            <a
+              onClick={() =>
+                onAuth(item, workHour[`${item.type_id}.${item.project_id}.${item.time}`])
+              }
+            >
               上报审批
             </a>
           </>
@@ -128,4 +142,4 @@ function WorkList(props) {
   );
 }
 
-export default Form.create()(WorkList);
+export default WorkList;

+ 18 - 32
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/index.js

@@ -1,24 +1,12 @@
 import React, { useState, useEffect, useRef } from 'react';
-import { Form } from '@ant-design/compatible';
-import '@ant-design/compatible/assets/index.css';
-import { Modal, Button, Calendar, Popover, Spin, Row, Col } from 'antd';
+import { Modal, Button, Calendar, Popover, Spin, Row, Col, message } from 'antd';
 import AddModal from './AddModal';
 import WorkList from './WorkList';
 import { connect } from 'dva';
 import moment from 'moment';
 
 function List(props) {
-  const {
-    typeList,
-    dispatch,
-    loading,
-    form,
-    dataList,
-    project,
-    projectList,
-    currentUser,
-    allType,
-  } = props;
+  const { typeList, dispatch, loading, dataList, projectList, currentUser, allType } = props;
   const [visible, setVisible] = useState(false);
   const [current, setCurrent] = useState({
     date: moment(),
@@ -26,7 +14,7 @@ function List(props) {
   });
 
   const onAuth = (item, workload) => {
-    console.log(workload);
+    if (workload == 0) return message.error('请上报有效工时');
     Modal.confirm({
       title: '提示',
       content: '是否上报审批?',
@@ -178,6 +166,13 @@ function List(props) {
     dispatch({
       type: 'workload/addWorkHours',
       payload: params,
+      callback: list => {
+        let time = current.date.format('YYYY-MM-DD');
+        setCurrent({
+          ...current,
+          list: list.filter(item => item.time == time),
+        });
+      },
     });
   };
 
@@ -265,7 +260,7 @@ function List(props) {
       if (!pid) return '';
       name = allType[pid].name;
     } else {
-      name = project?.find(p => p.ID == item.project_id)?.Name;
+      name = item.Project.Name;
     }
     return name;
   };
@@ -300,9 +295,9 @@ function List(props) {
       },
     });
     // 查询项目列表
-    dispatch({
-      type: 'workload/queryProject',
-    });
+    // dispatch({
+    //   type: 'workload/queryProject',
+    // });
     return () => {
       // 清空查询数据
       dispatch({
@@ -323,14 +318,14 @@ function List(props) {
     <div>
       <Spin spinning={loading}>
         <Row gutter={8}>
-          <Col span={12}>
+          <Col span={10}>
             <Calendar
               value={current.date}
               dateCellRender={dateCellRender}
               onChange={onChangeDate}
             />
           </Col>
-          <Col span={12}>
+          <Col span={14}>
             <div>
               <Button type="primary" style={{ marginBottom: 20 }} onClick={() => setVisible(true)}>
                 新增
@@ -353,20 +348,13 @@ function List(props) {
               )}
             </div>
 
-            <WorkList
-              allType={allType}
-              project={project}
-              list={current.list}
-              onAuth={onAuth}
-              onSave={onSave}
-            />
+            <WorkList allType={allType} list={current.list} onAuth={onAuth} onSave={onSave} />
           </Col>
         </Row>
       </Spin>
 
       <AddModal
         typeList={typeList}
-        projectList={projectList}
         visible={visible}
         onOk={onAddWork}
         time={current.date?.format('YYYY-MM-DD')}
@@ -380,8 +368,6 @@ export default connect(({ workload, user, loading }) => ({
   dataList: workload.dataList,
   typeList: workload.typeList,
   allType: workload.allType,
-  project: workload.project,
-  projectList: workload.projectList,
   currentUser: user.currentUser,
   loading: loading.models.workload,
-}))(Form.create()(List));
+}))(List);

+ 26 - 21
src/pages/PurchaseAdmin/PurchaseList/WorkingHours/models/workingHours.js

@@ -18,35 +18,43 @@ export default {
     dataList: [],
     typeList: [],
     allType: {},
+    subTypeList: [],
     project: [],
     projectList: { project0: [], project1: [] },
     filter: {},
   },
 
   effects: {
-    *queryWorkType({ payload, callback }, { call, put }) {
-      const { data } = yield call(queryWorkType, {});
+    *queryWorkType({ callback }, { call, put }) {
+      const { data } = yield call(queryWorkType, { parent_id: -1 });
+      let typeList = [];
       let allType = {};
       for (let i = 0; i < data.length; i++) {
         let item = data[i];
         allType[item.id] = item;
-        let { data: subData } = yield call(queryWorkType, { parent_id: item.id });
-        item.children = subData;
-        subData.forEach(item => {
-          allType[item.id] = item;
-        });
+        if (item.parent_id == 0) {
+          typeList.push(item);
+        }
       }
       yield put({
         type: 'save',
-        payload: { typeList: data, allType },
+        payload: { typeList, allType },
       });
       callback && callback();
     },
+    *querySubType({ payload, callback }, { call, put }) {
+      const { data } = yield call(queryWorkType, payload);
+      callback && callback(data);
+      yield put({
+        type: 'save',
+        payload: { subTypeList: data },
+      });
+    },
     /**
      *
      * payload = {s_time,e_time,user_id}
      */
-    *queryAuthWorkHours({ payload }, { call, put, select }) {
+    *queryAuthWorkHours({ payload, callback }, { call, put, select }) {
       const workload = yield select(s => s.workload);
       const { typeList, filter, allType } = workload;
       // 合并新旧过滤条件
@@ -80,6 +88,9 @@ export default {
       //     });
       //   });
       // });
+
+      callback && callback(data.list);
+
       yield put({
         type: 'save',
         payload: { list: data.list, filter: newFilter },
@@ -156,29 +167,23 @@ export default {
       const res = yield call(authWorkload, payload);
       if (res) {
         message.success('操作成功');
-        callback && callback();
         yield put({
           type: 'queryAuthWorkHours',
           payload: {},
+          callback,
         });
       }
     },
-    *queryProject({ callback }, { call, put }) {
-      const response0 = yield call(queryProject, { stage: 0 });
-      const response1 = yield call(queryProject, { stage: 1 });
-      const response7 = yield call(queryProject, { stage: 7 });
-      if (response0 && response1 && response7) {
+    *queryProject({ payload = {}, callback }, { call, put }) {
+      const res = yield call(queryProject, payload);
+      if (res) {
         yield put({
           type: 'save',
           payload: {
-            project: [...response0.data.list, ...response1.data.list, ...response7.data.list],
-            projectList: {
-              project0: response0.data.list,
-              project1: response1.data.list,
-              project7: response7.data.list,
-            },
+            project: res.data.list,
           },
         });
+        callback && callback();
       }
     },
   },

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