xjj 3 лет назад
Родитель
Сommit
21cda3e832
100 измененных файлов с 7657 добавлено и 425 удалено
  1. 3 0
      .eslintrc.js
  2. 4 0
      .gitignore
  3. 4 0
      .husky/commit-msg
  4. 4 0
      .husky/pre-commit
  5. 17 0
      .lintstagedrc
  6. 3 0
      .prettierignore
  7. 8 0
      .prettierrc
  8. 3 0
      .stylelintrc.js
  9. 43 0
      .umirc.ts
  10. 3 0
      README.md
  11. 0 18
      config/config.dev.ts
  12. 0 7
      config/config.prd.ts
  13. 0 12
      config/config.test.ts
  14. 20 0
      mock/userAPI.ts
  15. 19 14
      package.json
  16. 10 0
      src/access.ts
  17. 99 0
      src/app.ts
  18. 0 0
      src/assets/.gitkeep
  19. BIN
      src/assets/yay.jpg
  20. 49 0
      src/components/AuditForm/ComponentLibrary.js
  21. 92 0
      src/components/AuditForm/FormContent.js
  22. 326 0
      src/components/AuditForm/ItemAttribute.js
  23. 80 0
      src/components/AuditForm/constant.js
  24. 71 0
      src/components/AuditForm/index.js
  25. 35 0
      src/components/Flow/FlowchartNodePanel.js
  26. 261 0
      src/components/Flow/components/judgeComponent/index.tsx
  27. 76 0
      src/components/Flow/components/judgeModal/index.tsx
  28. 100 0
      src/components/Flow/config-cmd.ts
  29. 83 0
      src/components/Flow/config-keybinding.ts
  30. 145 0
      src/components/Flow/config-menu.ts
  31. 293 0
      src/components/Flow/config-toolbar.ts
  32. 3 0
      src/components/Flow/constant.ts
  33. 52 0
      src/components/Flow/index.less
  34. 323 0
      src/components/Flow/index.tsx
  35. 18 0
      src/components/Flow/node/FlowFormPanel.tsx
  36. 132 0
      src/components/Flow/node/auditNode/index.tsx
  37. 283 0
      src/components/Flow/node/auditNode/mapServe.tsx
  38. 44 0
      src/components/Flow/node/circle/index.tsx
  39. 181 0
      src/components/Flow/node/circle/mapServe.tsx
  40. 60 0
      src/components/Flow/node/constants.ts
  41. 10 0
      src/components/Flow/node/control-map-service/components/canvas.tsx
  42. 221 0
      src/components/Flow/node/control-map-service/components/edge.tsx
  43. 111 0
      src/components/Flow/node/control-map-service/components/group.tsx
  44. 119 0
      src/components/Flow/node/control-map-service/components/node.tsx
  45. 138 0
      src/components/Flow/node/control-map-service/components/style.less
  46. 18 0
      src/components/Flow/node/control-map-service/index.ts
  47. 106 0
      src/components/Flow/node/fields/color.tsx
  48. 92 0
      src/components/Flow/node/fields/imgSelect.tsx
  49. 23 0
      src/components/Flow/node/fields/index.ts
  50. 34 0
      src/components/Flow/node/fields/input-number.tsx
  51. 31 0
      src/components/Flow/node/fields/input.tsx
  52. 55 0
      src/components/Flow/node/fields/plcDevice.tsx
  53. 50 0
      src/components/Flow/node/fields/position.tsx
  54. 43 0
      src/components/Flow/node/fields/radio.tsx
  55. 48 0
      src/components/Flow/node/fields/select.tsx
  56. 36 0
      src/components/Flow/node/fields/size.tsx
  57. 39 0
      src/components/Flow/node/fields/upload.tsx
  58. 104 0
      src/components/Flow/node/formSchemaService.ts
  59. 122 0
      src/components/Flow/node/judgeNode/index.tsx
  60. 268 0
      src/components/Flow/node/judgeNode/mapServe.tsx
  61. 43 0
      src/components/Flow/node/rect/index.tsx
  62. 291 0
      src/components/Flow/node/rect/mapServe.tsx
  63. 45 0
      src/components/Flow/node/registerNode.tsx
  64. 34 0
      src/components/Flow/node/utils.ts
  65. 28 0
      src/components/Flow/react-node/CustomCircle.js
  66. 41 0
      src/components/Flow/react-node/CustomRect.js
  67. 24 0
      src/components/Flow/react-node/dnd-node.js
  68. 50 0
      src/components/Flow/service.ts
  69. 4 0
      src/components/Guide/Guide.less
  70. 23 0
      src/components/Guide/Guide.tsx
  71. 2 0
      src/components/Guide/index.ts
  72. 107 0
      src/components/OssUpload/AliyunOssUploader.js
  73. 1 0
      src/constants/index.ts
  74. 0 10
      src/layouts/index.less
  75. 0 10
      src/layouts/index.tsx
  76. 13 0
      src/models/global.ts
  77. 21 0
      src/pages/Access/index.tsx
  78. 128 0
      src/pages/Flow/Audit.js
  79. 16 0
      src/pages/Flow/Audit.less
  80. 39 0
      src/pages/Flow/AuditModal.js
  81. 101 0
      src/pages/Flow/index.js
  82. 291 0
      src/pages/Flow/models/flow.js
  83. 3 0
      src/pages/Home/index.less
  84. 18 0
      src/pages/Home/index.tsx
  85. 26 0
      src/pages/Table/components/CreateForm.tsx
  86. 138 0
      src/pages/Table/components/UpdateForm.tsx
  87. 270 0
      src/pages/Table/index.tsx
  88. 0 10
      src/pages/index.tsx
  89. 235 0
      src/services/SysAdmin.js
  90. 112 0
      src/services/approval.js
  91. 473 0
      src/services/boom.js
  92. 96 0
      src/services/demo/UserController.ts
  93. 7 0
      src/services/demo/index.ts
  94. 68 0
      src/services/demo/typings.d.ts
  95. 46 0
      src/services/user.js
  96. 95 0
      src/utils/event.js
  97. 4 0
      src/utils/format.ts
  98. 125 0
      src/utils/request.js
  99. 23 336
      src/utils/utils.js
  100. 1 8
      tsconfig.json

+ 3 - 0
.eslintrc.js

@@ -0,0 +1,3 @@
+module.exports = {
+  extends: require.resolve('@umijs/max/eslint'),
+};

+ 4 - 0
.gitignore

@@ -5,5 +5,9 @@
 /src/.umi
 /src/.umi-production
 /src/.umi-test
+/.umi
+/.umi-production
+/.umi-test
 /dist
+/.mfsu
 .swc

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npx --no-install max verify-commit $1

+ 4 - 0
.husky/pre-commit

@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npx --no-install lint-staged --quiet

+ 17 - 0
.lintstagedrc

@@ -0,0 +1,17 @@
+{
+  "*.{md,json}": [
+    "prettier --cache --write"
+  ],
+  "*.{js,jsx}": [
+    "max lint --fix --eslint-only",
+    "prettier --cache --write"
+  ],
+  "*.{css,less}": [
+    "max lint --fix --stylelint-only",
+    "prettier --cache --write"
+  ],
+  "*.ts?(x)": [
+    "max lint --fix --eslint-only",
+    "prettier --cache --parser=typescript --write"
+  ]
+}

+ 3 - 0
.prettierignore

@@ -0,0 +1,3 @@
+node_modules
+.umi
+.umi-production

+ 8 - 0
.prettierrc

@@ -0,0 +1,8 @@
+{
+  "printWidth": 80,
+  "singleQuote": true,
+  "trailingComma": "all",
+  "proseWrap": "never",
+  "overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
+  "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
+}

+ 3 - 0
.stylelintrc.js

@@ -0,0 +1,3 @@
+module.exports = {
+  extends: require.resolve('@umijs/max/stylelint'),
+};

+ 43 - 0
.umirc.ts

@@ -0,0 +1,43 @@
+import { defineConfig } from '@umijs/max';
+
+export default defineConfig({
+  antd: {},
+  dva: {},
+  access: {},
+  model: {},
+  initialState: {},
+  request: {},
+  layout: {
+    title: '@umijs/max',
+  },
+  proxy: {
+    '/api': {
+      // target: 'http://47.96.12.136:8788/',
+      target: 'http://47.96.12.136:8888/',
+      // target: 'http://120.55.44.4:8900/',
+      changeOrigin: true,
+    },
+  },
+  routes: [
+    {
+      path: '/',
+      redirect: '/home',
+    },
+    {
+      name: '首页',
+      path: '/home',
+      component: './Flow/index',
+    },
+    {
+      name: '权限演示',
+      path: '/access',
+      component: './Access',
+    },
+    {
+      name: ' CRUD 示例',
+      path: '/table',
+      component: './Table',
+    },
+  ],
+  npmClient: 'yarn',
+});

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# README
+
+`@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce)

+ 0 - 18
config/config.dev.ts

@@ -1,18 +0,0 @@
-import { defineConfig } from 'umi';
-export default defineConfig({
-  define: {
-    CurrentEnvironment: 'dev',
-  },
-  proxy: {
-    '/api': {
-      // target: 'http://192.168.20.53:8888/',
-      // target: 'http://120.55.44.4:8896/',
-      // target: 'http://47.96.12.136:8888/',
-      target: 'http://47.96.12.136:8896/',
-      // target: 'http://oraysmart.com:8889/',
-      // target: 'http://oraysmart.com:8888/api',
-      // changeOrigin: true,
-      // pathRewrite: { '^/api': '' },
-    }
-  }
-});

+ 0 - 7
config/config.prd.ts

@@ -1,7 +0,0 @@
-import { defineConfig } from 'umi';
-export default defineConfig({
-  define: {
-    CurrentEnvironment: 'prd',
-  },
-});
- 

+ 0 - 12
config/config.test.ts

@@ -1,12 +0,0 @@
-import { defineConfig } from 'umi';
-export default defineConfig({
-  define: {
-    CurrentEnvironment: 'test',
-  },
-  routes: [
-    { path: "/", component: "index" },
-
-  ],
-  npmClient: 'yarn',
-});
- 

+ 20 - 0
mock/userAPI.ts

@@ -0,0 +1,20 @@
+const users = [
+  { id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
+  { id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
+];
+
+export default {
+  'GET /api/v1/queryUserList': (req: any, res: any) => {
+    res.json({
+      success: true,
+      data: { list: users },
+      errorCode: 0,
+    });
+  },
+  'PUT /api/v1/user/': (req: any, res: any) => {
+    res.json({
+      success: true,
+      errorCode: 0,
+    });
+  },
+};

+ 19 - 14
package.json

@@ -1,26 +1,31 @@
 {
   "private": true,
-  "author": "Renxy <18510891294@163.com>",
+  "author": "xjj <645007605@qq.com>",
   "scripts": {
-    "dev": "umi dev",
-    "postinstall": "umi setup",
-    "setup": "umi setup",
-    "start": "cross-env UMI_ENV=dev umi dev",
-    "start:test": "cross-env UMI_ENV=test umi dev",
-    "start:prd": "cross-env UMI_ENV=prd umi dev",
-    "build": "cross-env UMI_ENV=dev umi build",
-    "build:test": "cross-env UMI_ENV=test umi build",
-    "build:prd": "cross-env UMI_ENV=prd umi build"
+    "dev": "max dev",
+    "build": "max build",
+    "format": "prettier --cache --write .",
+    "prepare": "husky install",
+    "postinstall": "max setup",
+    "setup": "max setup",
+    "start": "npm run dev"
   },
   "dependencies": {
-    "antd": "^5.4.0",
-    "umi": "^4.0.64"
+    "@ant-design/icons": "^4.7.0",
+    "@ant-design/pro-components": "^2.0.1",
+    "@umijs/max": "^4.0.64",
+    "antd": "^5.0.0",
+    "qs": "^6.11.1",
+    "umi-request": "^1.4.0"
   },
   "devDependencies": {
     "@types/react": "^18.0.0",
     "@types/react-dom": "^18.0.0",
-    "@umijs/plugin-request": "^2.9.0",
-    "cross-env": "^7.0.3",
+    "husky": "^8.0.1",
+    "lint-staged": "^13.0.3",
+    "prettier": "^2.7.1",
+    "prettier-plugin-organize-imports": "^2",
+    "prettier-plugin-packagejson": "^2",
     "typescript": "^5.0.0"
   }
 }

+ 10 - 0
src/access.ts

@@ -0,0 +1,10 @@
+export default (initialState: API.UserInfo) => {
+  // 在这里按照初始化数据定义项目中的权限,统一管理
+  // 参考文档 https://umijs.org/docs/max/access
+  const canSeeAdmin = !!(
+    initialState && initialState.name !== 'dontHaveAccess'
+  );
+  return {
+    canSeeAdmin,
+  };
+};

+ 99 - 0
src/app.ts

@@ -0,0 +1,99 @@
+// 运行时配置
+
+import { RequestConfig } from "@umijs/max";
+import { message } from "antd";
+
+// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
+// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
+export async function getInitialState(): Promise<{ name: string }> {
+  return { name: '@umijs/max' };
+}
+
+export const layout = () => {
+  return {
+    logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
+    menu: {
+      locale: false,
+    },
+  };
+};
+
+// axios配置
+export const request: RequestConfig = {
+  errorConfig: {
+    errorThrower(res: any) {
+      // const { data, code, msg } = res;
+      // if (code !== 200) {
+      //   const error: any = new Error(msg);
+      //   error.name = 'AjaxError';
+      //   error.info = { code, msg, data };
+      //   throw error;
+      // }
+      console.log('errorThrower', res);
+    },
+    errorHandler(error: any, opts: any) {
+      if (opts?.skipErrorHandler) throw error;
+      // errorThrower 抛出的错误。
+      if (error.name === 'AjaxError') {
+        // 校验是否token失效
+        if (tokenExpiredHandle(error.info.code)) return;
+        const errorInfo: any = error.info;
+        // const inLoginPage = window.GT_APP.funcLogin.IsActive;
+        // if (!inLoginPage && errorInfo) {
+        //   message.error(errorInfo.msg || errorInfo.data);
+        // }
+      } else if (error.response) {
+        // http错误,校验token是否失效
+        if (tokenExpiredHandle(error.response.status)) return;
+        // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
+        message.error(`网络连接错误,请稍后重试(${error.response.status})`);
+      } else if (error.request) {
+        // 请求已经成功发起,但没有收到响应
+        // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
+        // 而在node.js中是 http.ClientRequest 的实例
+        message.error('网络连接错误,请稍后重试(服务器未返回数据)。');
+      } else {
+        // 发送请求时出了点问题
+        message.error('网络连接错误,请稍后重试(请求发送失败)。');
+      }
+    },
+  },
+  requestInterceptors: [
+    (config: any) => {
+      if (!config.headers) config.headers = {};
+      config.headers['JWT-TOKEN'] = localStorage.token;
+      return config;
+    },
+  ],
+  responseInterceptors: [
+    (res: any) => {
+      const resData = res.data;
+      const { code, msg, data } = resData;
+      if (code !== 200) {
+        const error: any = new Error(msg);
+        error.name = 'AjaxError';
+        error.info = { code, msg, data };
+        throw error;
+      }
+      return res;
+    },
+  ],
+};
+let tokenFlag: boolean = false;
+// token失效校验
+const tokenExpiredHandle = (code: number) => {
+  if ([401, 601, 602, 603].includes(code)) {
+    if (tokenFlag) return true;
+
+    tokenFlag = true;
+    setTimeout(() => {
+      tokenFlag = false;
+    }, 50000);
+    message.error('token失效,请重新登录');
+   
+    return true;
+  } else {
+    return false;
+  }
+};
+

+ 0 - 0
src/assets/.gitkeep


BIN
src/assets/yay.jpg


+ 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;

+ 35 - 0
src/components/Flow/FlowchartNodePanel.js

@@ -0,0 +1,35 @@
+import { FlowchartNodePanel } from '@antv/xflow';
+/** 配置Dnd组件面板 */
+import CustomCircle from './react-node/CustomCircle';
+import CustomRect from './react-node/CustomRect';
+
+function NodePanel() {
+  return (
+    <FlowchartNodePanel
+      registerNode={{
+        title: '自定义节点',
+        key: '1',
+        nodes: [
+          {
+            component: CustomRect,
+            popover: () => <div>业务节点</div>,
+            name: 'custom-rect',
+            width: 120,
+            height: 50,
+            label: '业务节点',
+          },
+          {
+            component: CustomCircle,
+            popover: () => <div>审批节点</div>,
+            name: 'custom-circle',
+            width: 90,
+            height: 90,
+            label: '审批节点',
+          },
+        ],
+      }}
+      position={{ width: 162, top: 40, bottom: 0, left: 0 }}
+    />
+  );
+}
+export default NodePanel;

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

@@ -0,0 +1,261 @@
+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 "umi";
+
+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 },
+];
+export const SiginSmallOptions = [
+  { label: "<", value: 2 },
+  { label: "≤", value: 5 },
+];
+
+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
+                  value={judge?.condition?.smallSign == 2 ? 1 : 3}
+                  onChange={(value: number) => {
+                    const newValue = Number(value) == 1 ? 2 : 5;
+                    handleChange([], item, {
+                      ...judge?.condition,
+                      smallSign: newValue,
+                    });
+                  }}
+                >
+                  {SiginOptions.map((item) => (
+                    <Option key={item.value}>{item.label}</Option>
+                  ))}
+                </Select>
+                <span>N</span>
+                <Select
+                  value={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;

+ 100 - 0
src/components/Flow/config-cmd.ts

@@ -0,0 +1,100 @@
+import {
+  createCmdConfig,
+  DisposableCollection,
+  uuidv4,
+  NsGraphCmd,
+  XFlowGraphCommands,
+  IApplication,
+  IGraphPipelineCommand,
+  MODELS,
+  NsGraph,
+} from '@antv/xflow';
+// import { MockApi } from './service';
+
+export const useCmdConfig = props => {
+  const cmdConfig = createCmdConfig(config => {
+    config.setRegisterHookFn(hooks => {
+      const list = [
+        hooks.addNode.registerHook({
+          name: 'set node config',
+          handler: async args => {
+            args.nodeConfig.ports?.items.forEach(item => {
+              item.id = item.id || uuidv4().substring(0, 8);
+            });
+            args.nodeConfig = {
+              ...args.nodeConfig,
+              // ports
+              id: args.nodeConfig.id || `${uuidv4().substring(0, 8)}`,
+            };
+          },
+        }),
+        // hooks.addEdge.registerHook({
+        //   name: 'addEdge',
+        //   handler: async args => {
+        //     args.edgeConfig = {
+        //       ...args.edgeConfig,
+        //       id: `${uuidv4().substring(0, 8)}`,
+        //     };
+        //   },
+        // }),
+        hooks.selectNode.registerHook({
+          name: 'selectNode',
+          handler: async args => {
+            console.log('select node:', args);
+            props.onSelectNode?.(args);
+          },
+        }),
+        // hooks.updateNode.registerHook({
+        //   name: 'updateNode',
+        //   handler: async args => {
+        //     const { nodeConfig } = args;
+        //     props.onUpdate?.(nodeConfig);
+        //   },
+        // }),
+      ];
+      const toDispose = new DisposableCollection();
+      toDispose.pushAll(list);
+      return toDispose;
+    });
+  });
+
+  return cmdConfig();
+};
+
+/** 查询图的节点和边的数据 */
+export const initGraphCmds = (app: IApplication, graphData: NsGraph.IGraphData) => {
+  if (!app) return;
+  app.executeCommandPipeline([
+    /** 1. 从服务端获取数据 */
+    // {
+    //   commandId: XFlowGraphCommands.LOAD_DATA.id,
+    //   getCommandOption: async () => {
+    //     return {
+    //       args: {
+    //         loadDataService: MockApi.loadGraphData,
+    //       },
+    //     };
+    //   },
+    // } as IGraphPipelineCommand<NsGraphCmd.GraphLoadData.IArgs>,
+    /** 3. 画布内容渲染 */
+    {
+      commandId: XFlowGraphCommands.GRAPH_RENDER.id,
+      getCommandOption: async () => {
+        return {
+          args: {
+            graphData,
+          },
+        };
+      },
+    } as IGraphPipelineCommand<NsGraphCmd.GraphRender.IArgs>,
+    /** 4. 缩放画布 */
+    {
+      commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
+      getCommandOption: async () => {
+        return {
+          args: { factor: 'fit', zoomOptions: { maxScale: 1 } },
+        };
+      },
+    } as IGraphPipelineCommand<NsGraphCmd.GraphZoom.IArgs>,
+  ]);
+};

+ 83 - 0
src/components/Flow/config-keybinding.ts

@@ -0,0 +1,83 @@
+import { NsNodeCmd, NsGraphCmd, NsEdgeCmd } from '@antv/xflow'
+import {
+  createKeybindingConfig,
+  XFlowNodeCommands,
+  XFlowEdgeCommands,
+  XFlowGraphCommands,
+  MODELS,
+} from '@antv/xflow'
+
+export const useKeybindingConfig = createKeybindingConfig(config => {
+  // delete
+  config.setKeybindingFunc(regsitry => {
+    return regsitry.registerKeybinding([
+      {
+        id: 'delete node or edge',
+        keybinding: 'backspace',
+        callback: async function (item, modelService, cmd, e) {
+          const cells = await MODELS.SELECTED_CELLS.useValue(modelService)
+          cells.map(cell => {
+            if (cell.isNode()) {
+              return cmd.executeCommand<NsNodeCmd.DelNode.IArgs>(XFlowNodeCommands.DEL_NODE.id, {
+                nodeConfig: {
+                  ...cell.getData(),
+                  id: cell.id,
+                },
+              })
+            }
+            if (cell.isEdge()) {
+              return cmd.executeCommand<NsEdgeCmd.DelEdge.IArgs>(XFlowEdgeCommands.DEL_EDGE.id, {
+                edgeConfig: { ...cell.getData(), id: cell.id },
+              })
+            }
+          })
+        },
+      },
+      {
+        id: 'copy',
+        keybinding: ['command+c', 'ctrl+c'],
+        callback: async function (item, modelService, cmd, e) {
+          e.preventDefault()
+          console.log(item)
+          cmd.executeCommand<NsGraphCmd.GraphCopySelection.IArgs>(
+            XFlowGraphCommands.GRAPH_COPY.id,
+            {},
+          )
+        },
+      },
+      {
+        id: 'paste',
+        keybinding: ['command+v', 'ctrl+v'],
+        callback: async function (item, ctx, cmd, e) {
+          e.preventDefault()
+          cmd.executeCommand<NsGraphCmd.GraphPasteSelection.IArgs>(
+            XFlowGraphCommands.GRAPH_PASTE.id,
+            {},
+          )
+        },
+      },
+      {
+        id: 'undo',
+        keybinding: ['meta+z', 'ctrl+z'],
+        callback: async function (item, ctx, cmd, e) {
+          e.preventDefault()
+          cmd.executeCommand<NsGraphCmd.GraphHistoryUndo.IArgs>(
+            XFlowGraphCommands.GRAPH_HISTORY_UNDO.id,
+            {},
+          )
+        },
+      },
+      {
+        id: 'redo',
+        keybinding: ['meta+shift+z', 'ctrl+shift+z'],
+        callback: async function (item, ctx, cmd, e) {
+          e.preventDefault()
+          cmd.executeCommand<NsGraphCmd.GraphHistoryRedo.IArgs>(
+            XFlowGraphCommands.GRAPH_HISTORY_REDO.id,
+            {},
+          )
+        },
+      },
+    ])
+  })
+})

+ 145 - 0
src/components/Flow/config-menu.ts

@@ -0,0 +1,145 @@
+import {
+  NsNodeCmd,
+  NsEdgeCmd,
+  IMenuOptions,
+  NsGraph,
+  IArgsBase
+} from '@antv/xflow'
+import { createCtxMenuConfig, MenuItemType } from '@antv/xflow'
+import { IconStore, XFlowNodeCommands, XFlowEdgeCommands } from '@antv/xflow'
+import { DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons'
+
+import { IGraphCommand } from '@antv/xflow'
+
+/** 节点命令 */
+export namespace CustomCommands {
+  const category = '节点操作'
+  /** 清除画布 */
+  export const CLEAR_GRAPH: IGraphCommand = {
+    id: 'xflow:clear-graph',
+    label: '清除',
+    category,
+  }
+  /** 导出 */
+  export const EXPORT_GRAPH: IGraphCommand = {
+    id: 'xflow:export-graph',
+    label: '导出',
+    category,
+  }
+  /** 重命名节点弹窗 */
+  export const SHOW_RENAME_MODAL: IGraphCommand = {
+    id: 'xflow:rename-node-modal',
+    label: '打开重命名弹窗',
+    category,
+  }
+}
+export namespace NsRenameNodeCmd {
+  /** Command: 用于注册named factory */
+  export const command = CustomCommands.SHOW_RENAME_MODAL
+  /** hook name */
+  export const hookKey = 'renameNode'
+  /** hook 参数类型 */
+  export interface IArgs extends IArgsBase {
+    nodeConfig: NsGraph.INodeConfig
+    updateNodeNameService: IUpdateNodeNameService
+  }
+  export interface IUpdateNodeNameService {
+    (newName: string, nodeConfig: NsGraph.INodeConfig, meta: NsGraph.IGraphMeta): Promise<{
+      err: string | null
+      nodeName: string
+    }>
+  }
+  /** hook handler 返回类型 */
+  export interface IResult {
+    err: string | null
+    preNodeName?: string
+    currentNodeName?: string
+  }
+  /** hooks 类型 */
+  // export interface ICmdHooks extends IHooks {
+  //   renameNode: HookHub<IArgs, IResult>
+  // }
+}
+
+/** menuitem 配置 */
+export namespace NsMenuItemConfig {
+  /** 注册菜单依赖的icon */
+  IconStore.set('DeleteOutlined', DeleteOutlined)
+  IconStore.set('EditOutlined', EditOutlined)
+  IconStore.set('StopOutlined', StopOutlined)
+
+  export const DELETE_EDGE: IMenuOptions = {
+    id: XFlowEdgeCommands.DEL_EDGE.id,
+    label: '删除边',
+    iconName: 'DeleteOutlined',
+    onClick: async ({ target, commandService }) => {
+      commandService.executeCommand<NsEdgeCmd.DelEdge.IArgs>(XFlowEdgeCommands.DEL_EDGE.id, {
+        edgeConfig: target.data as NsGraph.IEdgeConfig,
+      })
+    },
+  }
+
+  export const DELETE_NODE: IMenuOptions = {
+    id: XFlowNodeCommands.DEL_NODE.id,
+    label: '删除节点',
+    iconName: 'DeleteOutlined',
+    onClick: async ({ target, commandService }) => {
+      commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(XFlowNodeCommands.DEL_NODE.id, {
+        nodeConfig: { id: target.data.id },
+      })
+    },
+  }
+
+  export const EMPTY_MENU: IMenuOptions = {
+    id: 'EMPTY_MENU_ITEM',
+    label: '暂无可用',
+    isEnabled: false,
+    iconName: 'DeleteOutlined',
+  }
+
+  export const SEPARATOR: IMenuOptions = {
+    id: 'separator',
+    type: MenuItemType.Separator,
+  }
+}
+
+export const useMenuConfig = createCtxMenuConfig(config => {
+  config.setMenuModelService(async (target, model, modelService, toDispose) => {
+    const { type, cell } = target
+    console.log(type)
+    switch (type) {
+      /** 节点菜单 */
+      case 'node':
+        model.setValue({
+          id: 'root',
+          type: MenuItemType.Root,
+          submenu: [NsMenuItemConfig.DELETE_NODE],
+        })
+        break
+      /** 边菜单 */
+      case 'edge':
+        model.setValue({
+          id: 'root',
+          type: MenuItemType.Root,
+          submenu: [NsMenuItemConfig.DELETE_EDGE],
+        })
+        break
+      /** 画布菜单 */
+      case 'blank':
+        model.setValue({
+          id: 'root',
+          type: MenuItemType.Root,
+          submenu: [NsMenuItemConfig.EMPTY_MENU],
+        })
+        break
+      /** 默认菜单 */
+      default:
+        model.setValue({
+          id: 'root',
+          type: MenuItemType.Root,
+          submenu: [NsMenuItemConfig.EMPTY_MENU],
+        })
+        break
+    }
+  })
+})

+ 293 - 0
src/components/Flow/config-toolbar.ts

@@ -0,0 +1,293 @@
+import {
+  createToolbarConfig,
+  IModelService,
+  IToolbarItemOptions,
+  NsGroupCmd,
+  uuidv4,
+  XFlowGroupCommands,
+  XFlowNodeCommands,
+  XFlowGraphCommands,
+  NsGraphCmd,
+  NsNodeCmd,
+  IconStore,
+  MODELS,
+} from '@antv/xflow';
+import {
+  UngroupOutlined,
+  SaveOutlined,
+  GroupOutlined,
+  GatewayOutlined,
+  UndoOutlined,
+  RedoOutlined,
+  VerticalAlignTopOutlined,
+  VerticalAlignBottomOutlined,
+  CopyOutlined,
+  SnippetsOutlined,
+} from '@ant-design/icons';
+
+const GROUP_NODE_RENDER_ID = 'GROUP_NODE_RENDER_ID';
+
+export namespace TOOLBAR_ITEMS {
+  export const BACK_NODE = XFlowNodeCommands.BACK_NODE.id;
+  export const FRONT_NODE = XFlowNodeCommands.FRONT_NODE.id;
+  export const SAVE_GRAPH_DATA = XFlowGraphCommands.SAVE_GRAPH_DATA.id;
+  export const REDO_CMD = `${XFlowGraphCommands.REDO_CMD.id}`;
+  export const UNDO_CMD = `${XFlowGraphCommands.UNDO_CMD.id}`;
+  export const MULTI_SELECT = `${XFlowGraphCommands.GRAPH_TOGGLE_MULTI_SELECT.id}`;
+  export const ADD_GROUP = `${XFlowGroupCommands.ADD_GROUP.id}`;
+  export const DEL_GROUP = `${XFlowGroupCommands.DEL_GROUP.id}`;
+  export const COPY = `${XFlowGraphCommands.GRAPH_COPY.id}`;
+  export const PASTE = `${XFlowGraphCommands.GRAPH_PASTE.id}`;
+}
+
+namespace NSToolbarConfig {
+  /** toolbar依赖的状态 */
+  export interface IToolbarState {
+    isMultiSelectionActive: boolean;
+    isGroupSelected: boolean;
+    isNodeSelected: boolean;
+    isUndoable: boolean;
+    isRedoable: boolean;
+  }
+
+  export const getDependencies = async (modelService: IModelService) => {
+    return [
+      await MODELS.SELECTED_NODES.getModel(modelService),
+      await MODELS.GRAPH_ENABLE_MULTI_SELECT.getModel(modelService),
+    ];
+  };
+
+  /** toolbar依赖的状态 */
+  export const getToolbarState = async (modelService: IModelService) => {
+    // isMultiSelectionActive
+    const { isEnable: isMultiSelectionActive } = await MODELS.GRAPH_ENABLE_MULTI_SELECT.useValue(
+      modelService
+    );
+    // isGroupSelected
+    const isGroupSelected = await MODELS.IS_GROUP_SELECTED.useValue(modelService);
+    // isNormalNodesSelected: node不能是GroupNode
+    const isNormalNodesSelected = await MODELS.IS_NORMAL_NODES_SELECTED.useValue(modelService);
+    // undo redo
+    const isUndoable = await MODELS.COMMAND_UNDOABLE.useValue(modelService);
+    const isRedoable = await MODELS.COMMAND_REDOABLE.useValue(modelService);
+
+    return {
+      isUndoable,
+      isRedoable,
+      isNodeSelected: isNormalNodesSelected,
+      isGroupSelected,
+      isMultiSelectionActive,
+    } as NSToolbarConfig.IToolbarState;
+  };
+
+  export const getToolbarItems = async (state: IToolbarState) => {
+    const toolbarGroup: IToolbarItemOptions[] = [];
+    // const history = getGraphHistory()
+
+    // /** 撤销 */
+    // toolbarGroup.push({
+    //   tooltip: '撤销',
+    //   iconName: 'UndoOutlined',
+    //   id: TOOLBAR_ITEMS.UNDO_CMD,
+    //   isEnabled: history.canUndo(),
+    //   onClick: async () => {
+    //     history.undo()
+    //   },
+    // })
+
+    // /** 重做 */
+    // toolbarGroup.push({
+    //   tooltip: '重做',
+    //   iconName: 'RedoOutlined',
+    //   id: TOOLBAR_ITEMS.REDO_CMD,
+    //   isEnabled: history.canRedo(),
+    //   onClick: async () => {
+    //     history.redo()
+    //   },
+    // })
+
+    /** FRONT_NODE */
+    toolbarGroup.push({
+      tooltip: '置前',
+      iconName: 'VerticalAlignTopOutlined',
+      id: TOOLBAR_ITEMS.FRONT_NODE,
+      isEnabled: state.isNodeSelected,
+      onClick: async ({ commandService, modelService }) => {
+        const node = await MODELS.SELECTED_NODE.useValue(modelService);
+        commandService.executeCommand<NsNodeCmd.FrontNode.IArgs>(TOOLBAR_ITEMS.FRONT_NODE, {
+          nodeId: node?.id,
+        });
+      },
+    });
+
+    /** BACK_NODE */
+    toolbarGroup.push({
+      tooltip: '置后',
+      iconName: 'VerticalAlignBottomOutlined',
+      id: TOOLBAR_ITEMS.BACK_NODE,
+      isEnabled: state.isNodeSelected,
+      onClick: async ({ commandService, modelService }) => {
+        const node = await MODELS.SELECTED_NODE.useValue(modelService);
+        commandService.executeCommand<NsNodeCmd.FrontNode.IArgs>(TOOLBAR_ITEMS.BACK_NODE, {
+          nodeId: node?.id,
+        });
+      },
+    });
+
+    /** 开启框选 */
+    toolbarGroup.push({
+      tooltip: '开启框选',
+      iconName: 'GatewayOutlined',
+      id: TOOLBAR_ITEMS.MULTI_SELECT,
+      active: state.isMultiSelectionActive,
+      onClick: async ({ commandService }) => {
+        commandService.executeCommand<NsGraphCmd.GraphToggleMultiSelect.IArgs>(
+          TOOLBAR_ITEMS.MULTI_SELECT,
+          {}
+        );
+      },
+    });
+
+    /** 新建群组 */
+    toolbarGroup.push({
+      tooltip: '新建群组',
+      iconName: 'GroupOutlined',
+      id: TOOLBAR_ITEMS.ADD_GROUP,
+      isEnabled: state.isNodeSelected,
+      onClick: async ({ commandService, modelService }) => {
+        const cells = await MODELS.SELECTED_CELLS.useValue(modelService);
+        const groupChildren = cells.map(cell => cell.id);
+        commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(TOOLBAR_ITEMS.ADD_GROUP, {
+          nodeConfig: {
+            id: uuidv4(),
+            renderKey: GROUP_NODE_RENDER_ID,
+            groupChildren,
+            groupCollapsedSize: { width: 200, height: 40 },
+            label: '新建群组',
+          },
+        });
+      },
+    });
+
+    /** 解散群组 */
+    toolbarGroup.push({
+      tooltip: '解散群组',
+      iconName: 'UngroupOutlined',
+      id: TOOLBAR_ITEMS.DEL_GROUP,
+      isEnabled: state.isGroupSelected,
+      onClick: async ({ commandService, modelService }) => {
+        const cell = await MODELS.SELECTED_NODE.useValue(modelService);
+        const nodeConfig = cell.getData();
+        commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.DEL_GROUP.id, {
+          nodeConfig: nodeConfig,
+        });
+      },
+    });
+
+    /** 保存数据 */
+    toolbarGroup.push({
+      tooltip: '保存',
+      iconName: 'SaveOutlined',
+      id: TOOLBAR_ITEMS.SAVE_GRAPH_DATA,
+      onClick: async ({ commandService }) => {
+        commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
+          TOOLBAR_ITEMS.SAVE_GRAPH_DATA,
+          {
+            saveGraphDataService: (meta, graphData) => {
+              let data = JSON.parse(JSON.stringify(graphData));
+              data.nodes = data.nodes.map(item => {
+                // delete item.incomingEdges;
+                // delete item.originData;
+                // delete item.outgoingEdges;
+                // delete item.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,
+                  flow_id: item.flow_id || 0,
+                  node_type: item.name == 'custom-circle' ? 1 : 0,
+                  count: 0,
+                  role_list: item.role_list,
+                  // count: item.count,
+                };
+              });
+              // 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(data);
+              console.log(JSON.stringify(data));
+              localStorage.graphData = JSON.stringify(data);
+              return null;
+            },
+          }
+        );
+      },
+    });
+    return [
+      {
+        name: 'graphData',
+        items: toolbarGroup,
+      },
+    ];
+  };
+}
+
+/** 注册icon 类型 */
+const registerIcon = () => {
+  IconStore.set('SaveOutlined', SaveOutlined);
+  IconStore.set('UndoOutlined', UndoOutlined);
+  IconStore.set('RedoOutlined', RedoOutlined);
+  IconStore.set('VerticalAlignTopOutlined', VerticalAlignTopOutlined);
+  IconStore.set('VerticalAlignBottomOutlined', VerticalAlignBottomOutlined);
+  IconStore.set('GatewayOutlined', GatewayOutlined);
+  IconStore.set('GroupOutlined', GroupOutlined);
+  IconStore.set('UngroupOutlined', UngroupOutlined);
+  IconStore.set('CopyOutlined', CopyOutlined);
+  IconStore.set('SnippetsOutlined', SnippetsOutlined);
+};
+
+export const useToolbarConfig = createToolbarConfig((toolbarConfig, proxy) => {
+  registerIcon();
+  /** 生产 toolbar item */
+  toolbarConfig.setToolbarModelService(async (toolbarModel, modelService, toDispose) => {
+    const updateToolbarModel = async () => {
+      const state = await NSToolbarConfig.getToolbarState(modelService);
+      const toolbarItems = await NSToolbarConfig.getToolbarItems(state);
+
+      toolbarModel.setValue(toolbar => {
+        toolbar.mainGroups = toolbarItems;
+      });
+    };
+    const models = await NSToolbarConfig.getDependencies(modelService);
+    const subscriptions = models.map(model => {
+      return model.watch(async () => {
+        updateToolbarModel();
+      });
+    });
+    toDispose.pushAll(subscriptions);
+  });
+});

+ 3 - 0
src/components/Flow/constant.ts

@@ -0,0 +1,3 @@
+export const DND_RENDER_ID = 'DND_NDOE'
+export const NODE_WIDTH = 200
+export const NODE_HEIGHT = 60

+ 52 - 0
src/components/Flow/index.less

@@ -0,0 +1,52 @@
+@body-bg: #fff;
+@primaryColor: #1890ff;
+@light-border: 1px solid #d9d9d9;
+
+:global {
+  .flow-user-custom-clz {
+    position: relative;
+    height: 600px;
+    border: @light-border;
+
+    .xflow-x6-canvas {
+      background: @body-bg;
+    }
+
+    .xflow-node-dnd-panel-node-wrapper {
+      .xflow-dnd-node {
+        width: 190px;
+      }
+    }
+
+    .xflow-canvas-dnd-node-tree {
+      border-right: @light-border;
+    }
+
+    .xflow-workspace-toolbar-top {
+      border-bottom: @light-border;
+    }
+
+    .xflow-workspace-toolbar-bottom {
+      text-align: center;
+      background: #fff;
+      border-top: @light-border;
+    }
+  }
+
+  .xflow-modal-container {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1000;
+  }
+  .xflow-json-schema-form-footer {
+    display: none;
+  }
+  .flowchart-demo {
+    .__dumi-default-previewer-actions {
+      border: 0;
+    }
+  }
+}

+ 323 - 0
src/components/Flow/index.tsx

@@ -0,0 +1,323 @@
+import { IAppLoad, NsGraphCmd } from '@antv/xflow';
+import React, { useRef, useEffect, useImperativeHandle } from 'react';
+/** 交互组件 */
+import {
+  /** XFlow核心组件 */
+  XFlow,
+  /** 流程图画布组件 */
+  FlowchartCanvas,
+  /** 流程图配置扩展 */
+  FlowchartExtension,
+  /** 流程图节点组件 */
+  FlowchartNodePanel,
+  /** 流程图表单组件 */
+  FlowchartFormPanel,
+  /** 通用组件:快捷键 */
+  KeyBindings,
+  /** 通用组件:画布缩放 */
+  CanvasScaleToolbar,
+  /** 通用组件:右键菜单 */
+  CanvasContextMenu,
+  /** 通用组件:工具栏 */
+  CanvasToolbar,
+  /** 通用组件:对齐线 */
+  CanvasSnapline,
+  /** 通用组件:节点连接桩 */
+  CanvasNodePortTooltip,
+  IApplication,
+  XFlowNodeCommands,
+} from '@antv/xflow';
+import { Graph } from '@antv/x6';
+/** 配置Command*/
+import { useCmdConfig, initGraphCmds } from './config-cmd';
+/** 配置Menu */
+import { useMenuConfig } from './config-menu';
+/** 配置Toolbar */
+import { TOOLBAR_ITEMS, useToolbarConfig } from './config-toolbar';
+/** 配置快捷键 */
+import { useKeybindingConfig } from './config-keybinding';
+import { registerNode } from './node/registerNode';
+import CustomFlowchartFormPanel from './node/FlowFormPanel';
+/** 配置Dnd组件面板 */
+// import CustomCircle from './react-node/CustomCircle';
+// import CustomRect from './react-node/CustomRect';
+
+import '@antv/xflow/dist/index.css';
+import { Collapse } from "antd"
+import './index.less';
+import { TYPE } from './node/auditNode/mapServe';
+
+const { Panel } = Collapse
+
+export interface IProps {
+  meta: {
+    flowId: string;
+    type: 'edit';
+    editMode?: number; // 1. 管理员编辑   2. 普通编辑
+  };
+  flowDetail: any;
+  onSelectNode?: Function;
+  parentRef?: any;
+}
+
+export const Demo: React.FC<IProps> = props => {
+  const { meta, flowDetail, parentRef } = props;
+  const isEdit = meta.type == 'edit';
+  const toolbarConfig = useToolbarConfig();
+  const menuConfig = useMenuConfig();
+  const keybindingConfig = useKeybindingConfig();
+  const graphRef = useRef<Graph>();
+  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配置项
+   */
+  const onLoad: IAppLoad = async app => {
+    appRef.current = app;
+    graphRef.current = await app.getGraphInstance();
+    // graphRef.current.disableSnapline()
+    renderGraph();
+  };
+
+  const renderGraph = () => {
+    if (flowDetail.nodes.length == 0 || !appRef.current) return;
+    initGraphCmds(appRef.current, flowDetail);
+    // 设置选中状态
+    const node = flowDetail.nodes.find(item => item.isCheck);
+    if (node) {
+      const args = {
+        nodeIds: [node.id],
+        resetSelection: true,
+      };
+      setTimeout(() => {
+        appRef.current.commandService.executeCommand(XFlowNodeCommands.SELECT_NODE.id, args);
+      }, 100);
+    }
+  };
+  const getConfig = () => {
+    const defaultOption = {
+      grid: 1,
+      mousewheel: {
+        enabled: true,
+        /** 将鼠标位置作为中心缩放 */
+        zoomAtMousePosition: true,
+      },
+      resizing: {
+        enabled: true,
+        minWidth: 0,
+        minHeight: 0,
+        preserveAspectRatio: false,
+      },
+      snapline: false,
+    };
+    if (isEdit) {
+      // 非管理员编辑
+      if (meta.editMode == 2) {
+        return {
+          ...defaultOption,
+          grid: false,
+          selecting: {
+            enabled: true,
+            showNodeSelectionBox: true,
+          },
+          interacting: false,
+          mousewheel: false,
+          connecting: {
+            highlight: false,
+            allowBlank: false,
+            allowPort: false,
+            dangling: false,
+          },
+        };
+      } else {
+        // 管理员编辑
+        return defaultOption;
+      }
+    } else {
+      return {
+        ...defaultOption,
+        grid: false,
+        resizing: false,
+        panning: false,
+        selecting: {
+          enabled: true,
+          showNodeSelectionBox: true,
+          movable: false,
+        },
+        interacting: false,
+        mousewheel: false,
+        connecting: {
+          highlight: false,
+          allowBlank: false,
+          allowPort: false,
+          dangling: false,
+        },
+      };
+    }
+  };
+  useEffect(() => {
+    if (graphRef.current) {
+      graphRef.current.on('node:click', (...arg) => {
+        console.log(arg);
+      });
+    }
+  }, [graphRef]);
+
+  useEffect(() => {
+    renderGraph();
+  }, [flowDetail, appRef.current]);
+
+  return (
+    <XFlow
+      className="flow-user-custom-clz"
+      commandConfig={commandConfig}
+      onLoad={onLoad}
+      meta={meta}
+    >
+      <FlowchartNodePanel
+        registerNode={{
+          title: '节点',
+          key: 'custom',
+          nodes: registerNode,
+        }}
+        showOfficial={false}
+        defaultActiveKey={['custom']}
+        position={{ width: 162, top: 40, bottom: 0, left: isEdit && meta.editMode == 1 ? 0 : -999 }}
+      />
+      {isEdit && (
+        <CanvasToolbar
+          className="xflow-workspace-toolbar-top"
+          layout="horizontal"
+          config={toolbarConfig}
+          position={{ top: 0, left: 0, right: 0, bottom: 0 }}
+        />
+      )}
+      <FlowchartCanvas
+        config={getConfig()}
+        position={{ top: isEdit ? 40 : 0, left: 0, right: 0, bottom: 0 }}
+      >
+        {isEdit && (
+          <>
+            <CanvasScaleToolbar
+              layout="horizontal"
+              position={{ top: -40, right: 0 }}
+              style={{
+                width: 150,
+                left: 'auto',
+                height: 39,
+              }}
+            />
+            <CanvasContextMenu config={menuConfig} />
+            <CanvasSnapline color="#faad14" />
+            <CanvasNodePortTooltip />
+          </>
+        )}
+      </FlowchartCanvas>
+      {isEdit && (
+        <>
+          <FlowchartExtension />
+          {/* <FlowchartFormPanel show={true} position={{ width: 200, top: 40, bottom: 0, right: 0 }} /> */}
+          <CustomFlowchartFormPanel />
+          <KeyBindings config={keybindingConfig} />
+        </>
+      )}
+    </XFlow>
+  );
+};
+
+// 高阶组件
+const DemoHoc = Demo => {
+  const forwardRef = (props, ref) => {
+    return <Demo parentRef={ref} {...props}></Demo>;
+  };
+  return React.forwardRef(forwardRef);
+};
+
+export default DemoHoc(Demo);

+ 18 - 0
src/components/Flow/node/FlowFormPanel.tsx

@@ -0,0 +1,18 @@
+import React, { useState, useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import { FlowchartFormPanel } from '@antv/xflow';
+import controlMapService from './control-map-service';
+import formSchemaService from './formSchemaService';
+
+const CustomFlowchartFormPanel = () => {
+  return (
+    <FlowchartFormPanel
+      show={true}
+      position={{ width: 240, top: 40, bottom: 0, right: 0 }}
+      controlMapService={controlMapService}
+      formSchemaService={formSchemaService}
+    ></FlowchartFormPanel>
+  );
+};
+
+export default CustomFlowchartFormPanel;

+ 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 "umi";
+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.origin, 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 "umi";
+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);

+ 44 - 0
src/components/Flow/node/circle/index.tsx

@@ -0,0 +1,44 @@
+import React from 'react';
+import CircleServe from './mapServe';
+import { Badge } from 'antd';
+import { useXFlowApp, XFlowNodeCommands } from '@antv/xflow';
+export { CircleServe };
+
+export default function CustomRect(props) {
+  const { size = { width: 90, height: 90 }, data } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize } = data;
+  const app = useXFlowApp();
+  const handleClick = () => {
+    // console.log(data);
+    app.executeCommand(XFlowNodeCommands.SELECT_NODE.id, {
+      nodeId: data.id,
+    });
+    // console.log('XFlowNodeCommands.SELECT_NODE.id', data);
+    // message.success(`${XFlowNodeCommands.SELECT_NODE.label}: 命令执行成功`);
+  };
+  return (
+    <Badge count={data.versions} offset={[-10, 10]}>
+      <div
+        className="container"
+        onClick={handleClick}
+        style={{
+          width,
+          height,
+          border: `2px solid ${stroke || '#FEDDAE'}`,
+          backgroundColor: fill || '#FEF7E7',
+          color: fontFill,
+          fontSize,
+          borderRadius: '50%',
+          display: 'flex',
+          justifyContent: 'center',
+          alignItems: 'center',
+          padding: "0px 8px",
+          lineHeight: 1.2
+        }}
+      >
+        <span style={{ textAlign: 'center', lineHeight: '20px' }}>{label}</span>
+      </div>
+    </Badge>
+  );
+}

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

@@ -0,0 +1,181 @@
+import React, { useState, useEffect } from 'react';
+import { FlowchartFormWrapper, MODELS, useXFlowApp } from '@antv/xflow';
+import { Position, Size, ColorPicker, InputNumberFiled, InputFiled, SelectField } from '../fields';
+import { PREFIX } from '../constants';
+import { connect } from "umi";
+import { UnityAction } from '@/utils/utils';
+import { Button } from 'antd';
+import { IProps } from '@/components/Flow/index';
+
+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;
+}
+
+const Component = (props: any) => {
+  const { config, plugin = {}, auditList } = props;
+  const { updateNode } = plugin;
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...config,
+  });
+  const app = useXFlowApp();
+  const [meta, setMeta] = useState<IProps['meta']>(null);
+  const onNodeConfigChange = (key: string, value: number | string | object) => {
+    if (key) {
+      setNodeConfig({
+        ...nodeConfig,
+        [key]: value,
+      });
+      updateNode({
+        [key]: value,
+      });
+    } else if (value instanceof Object) {
+      setNodeConfig({
+        ...nodeConfig,
+        ...value,
+      });
+      updateNode({
+        ...value,
+      });
+    }
+  };
+
+  const onSave = () => {
+    UnityAction.emit('NODE_SAVE', nodeConfig);
+  };
+  useEffect(() => {
+    setNodeConfig({
+      ...config,
+    });
+  }, [config]);
+
+  useEffect(() => {
+    MODELS.GRAPH_META.useValue(app.modelService).then((meta: IProps['meta']) => {
+      setMeta(meta);
+    });
+  }, []);
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={nodeConfig.label}
+          disabled={meta?.editMode == 2}
+          onChange={value => {
+            onNodeConfigChange('label', value);
+          }}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>数据</h5>
+        <SelectField
+          label="审批流程"
+          value={nodeConfig.flow_id}
+          disabled={meta?.editMode == 2}
+          onChange={value => {
+            let audit = auditList.find(item => item.list.id == value);
+
+            onNodeConfigChange(null, {
+              flow_node_id: audit.list.FlowNodes[0]?.id,
+              flow_id: value,
+              process_code: audit.list.process_code,
+            });
+          }}
+          options={auditList.map(item => {
+            const list = item.list;
+            return {
+              label: list.name || list.id,
+              value: list.id,
+            };
+          })}
+        />
+      </div>
+
+      {meta?.editMode != 2 && (
+        <>
+          <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>
+
+          <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>
+          <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 }))(RecthServe);

+ 60 - 0
src/components/Flow/node/constants.ts

@@ -0,0 +1,60 @@
+export const PREFIX = 'flowchart-editor';
+export const FormItemHeight = 24;
+/** 边默认配置 */
+export const DefaultEdgeConfig = {
+  attrs: {
+    line: {
+      stroke: '#A2B1C3',
+      strokeWidth: 1,
+      strokeDasharray: [0, 0],
+    },
+    text: {
+      fill: '#000',
+      fontSize: 12,
+      textAnchor: 'middle',
+      textVerticalAnchor: 'middle',
+    },
+  },
+};
+export const DefaultNodeConfig = {
+  stroke: '#A2B1C3',
+  fill: '#FFFFFF',
+  fontFill: '#000',
+  fontSize: 12,
+  label: '',
+};
+export const ArrowConfig = {
+  width: 12,
+  height: 8,
+  name: 'block',
+};
+
+export const DisableArrowConfig = {
+  width: 0,
+  height: 0,
+  name: '',
+};
+
+export const ArrowMaps = {
+  target: {
+    sourceMarker: DisableArrowConfig,
+    targetMarker: ArrowConfig,
+  },
+  source: {
+    sourceMarker: ArrowConfig,
+    targetMarker: DisableArrowConfig,
+  },
+  all: {
+    sourceMarker: ArrowConfig,
+    targetMarker: ArrowConfig,
+  },
+  none: {
+    sourceMarker: DisableArrowConfig,
+    targetMarker: DisableArrowConfig,
+  },
+};
+
+export const ArrowStrokeMaps = {
+  solid: [0, 0],
+  dash: [5, 5],
+};

+ 10 - 0
src/components/Flow/node/control-map-service/components/canvas.tsx

@@ -0,0 +1,10 @@
+import React from 'react';
+import { PREFIX } from '../../constants';
+
+export const CanvasService: React.FC = () => {
+  return (
+    <div className={`${PREFIX}-canvas-panel`}>
+      <span>未选中</span>
+    </div>
+  );
+};

+ 221 - 0
src/components/Flow/node/control-map-service/components/edge.tsx

@@ -0,0 +1,221 @@
+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';
+
+export type MarkerCfg = {
+  width?: number;
+  height?: number;
+  name?: string;
+};
+export interface IConfig {
+  label?: string;
+  attrs?: {
+    line?: {
+      strokeWidth?: number;
+      sourceMarker?: MarkerCfg;
+      targetMarker?: MarkerCfg;
+      strokeDasharray?: String;
+    };
+    text?: {
+      fontSize?: number;
+      fill?: string;
+    };
+  };
+}
+
+const EdgeComponent = (props: any) => {
+  const { config, plugin = {} } = props;
+  const { updateEdge } = plugin;
+
+  const [edgeConfig, setEdgeConfig] = useState<any>({
+    // const [edgeConfig, setEdgeConfig] = useState<IConfig>({
+    ...DefaultEdgeConfig,
+    ...config,
+  });
+  console.log(edgeConfig);
+
+  useEffect(() => {
+    setEdgeConfig({
+      ...DefaultEdgeConfig,
+      ...config,
+    });
+  }, [config]);
+
+  const getAttrs = (key: string, type = 'line') => {
+    const { attrs = {} } = edgeConfig;
+    return attrs[type]?.[key];
+  };
+
+  const getArrowValue = () => {
+    const { attrs = {} } = edgeConfig;
+    const { line = {} } = attrs;
+    if (line.sourceMarker?.name && line.targetMarker?.name) {
+      return 'all';
+    }
+    if (!line.sourceMarker?.name && !line.targetMarker?.name) {
+      return 'none';
+    }
+    if (line.sourceMarker?.name) {
+      return 'source';
+    }
+    return 'target';
+  };
+
+  const getSrokeDashValue = () => {
+    const { attrs = {} } = edgeConfig;
+    const { line = {} } = attrs;
+    return line.strokeDasharray && line.strokeDasharray[0] == 5 ? 'dash' : 'solid';
+  };
+
+  const onEdgeConfigChange = (
+    key: string,
+    value: number | string | object,
+    type: string = 'line'
+  ) => {
+    /** 全量更新,简化逻辑 */
+    if (key === 'arrow') {
+      setEdgeConfig({
+        ...edgeConfig,
+        attrs: {
+          ...edgeConfig.attrs,
+          [type]: {
+            ...edgeConfig.attrs?.[type],
+            ...(value as object),
+          },
+        },
+      });
+    } else {
+      setEdgeConfig({
+        ...edgeConfig,
+        [key]: value,
+        attrs: {
+          ...edgeConfig.attrs,
+          [type]: {
+            ...edgeConfig.attrs?.[type],
+            [key]: value,
+          },
+        },
+      });
+    }
+
+    updateEdge(
+      {
+        [key]: value,
+      },
+      type,
+      key === 'arrow' ? 'arrow' : ''
+    );
+  };
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标签"
+          value={edgeConfig.label}
+          onChange={value => {
+            onEdgeConfigChange('label', value);
+          }}
+        />
+      </div>
+      <h5 style={{ marginBottom: 12 }}>样式</h5>
+      <div className={`${PREFIX}-panel-group`} style={{ marginBottom: 0 }}>
+        <h5>线</h5>
+        <SelectField
+          label="箭头"
+          value={getArrowValue()}
+          width="100%"
+          options={[
+            {
+              label: '正向',
+              value: 'target',
+            },
+            {
+              label: '逆向',
+              value: 'source',
+            },
+            {
+              label: '双向',
+              value: 'all',
+            },
+            {
+              label: '无',
+              value: 'none',
+            },
+          ]}
+          onChange={value => {
+            onEdgeConfigChange('arrow', ArrowMaps[value as keyof typeof ArrowMaps], 'line');
+          }}
+        />
+
+        <div className={`${PREFIX}-edge-stroke-style`}>
+          <SelectField
+            label="线形"
+            width={68}
+            value={getSrokeDashValue()}
+            options={[
+              {
+                label: '实线',
+                value: 'solid',
+              },
+              {
+                label: '虚线',
+                value: 'dash',
+              },
+            ]}
+            onChange={value => {
+              onEdgeConfigChange(
+                'strokeDasharray',
+                ArrowStrokeMaps[value as keyof typeof ArrowStrokeMaps],
+                'line'
+              );
+            }}
+          />
+          <InputNumberFiled
+            value={getAttrs('strokeWidth')}
+            min={1}
+            onChange={value => {
+              onEdgeConfigChange('strokeWidth', value, 'line');
+            }}
+          />
+        </div>
+        <ColorPicker
+          label="边框"
+          value={getAttrs('stroke')}
+          onChange={(value: string) => {
+            onEdgeConfigChange('stroke', value, 'line');
+          }}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>标签</h5>
+        <div className={`${PREFIX}-edge-text-style`}>
+          <InputNumberFiled
+            label="字号"
+            min={10}
+            width={68}
+            value={getAttrs('fontSize', 'text') || 12}
+            onChange={value => {
+              onEdgeConfigChange('fontSize', value, 'text');
+            }}
+          />
+          <ColorPicker
+            value={getAttrs('fill', 'text') || '#000'}
+            onChange={(value: string) => {
+              onEdgeConfigChange('fill', value, 'text');
+            }}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+export const EdgeService: React.FC<any> = props => {
+  return (
+    <FlowchartFormWrapper {...props} type="edge">
+      {(config, plugin) => <EdgeComponent {...props} plugin={plugin} config={config} />}
+    </FlowchartFormWrapper>
+  );
+};

+ 111 - 0
src/components/Flow/node/control-map-service/components/group.tsx

@@ -0,0 +1,111 @@
+import React, { useState, useEffect } from 'react';
+import { FlowchartFormWrapper } from '@antv/xflow';
+import {
+  InputFiled,
+  ColorPicker,
+  Position,
+  InputNumberFiled,
+  Size,
+} from '../../fields';
+import  { IConfig } from './node';
+
+import { PREFIX, DefaultNodeConfig } from '../../constants';
+
+const GroupComponent = (props: any) => {
+  const { config, plugin = {} } = props;
+  const { updateGroup } = plugin;
+
+  const [groupConfig, setGroupConfig] = useState<IConfig>({
+    ...DefaultNodeConfig,
+    ...config,
+  });
+
+  const onGroupConfigChange = (key: string, value: number | string) => {
+    setGroupConfig({
+      ...groupConfig,
+      [key]: value,
+    });
+    updateGroup({
+      [key]: value,
+    });
+  };
+
+  useEffect(() => {
+    setGroupConfig({
+      ...DefaultNodeConfig,
+      ...config,
+    });
+  }, [config]);
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={groupConfig.label}
+          onChange={(value) => {
+            onGroupConfigChange('label', value);
+          }}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>样式</h5>
+        <Position
+          x={groupConfig.x}
+          y={groupConfig.y}
+          onChange={(key, value) => {
+            onGroupConfigChange(key, value);
+          }}
+        />
+        <Size
+          width={groupConfig.width}
+          height={groupConfig.height}
+          onChange={(key, value) => {
+            onGroupConfigChange(key, value);
+          }}
+        />
+        <ColorPicker
+          label="填充"
+          value={groupConfig.fill}
+          onChange={(value: string) => {
+            onGroupConfigChange('fill', value);
+          }}
+        />
+        <ColorPicker
+          label="边框"
+          value={groupConfig.stroke}
+          onChange={(value: string) => {
+            onGroupConfigChange('stroke', value);
+          }}
+        />
+        <div className={`${PREFIX}-node-text-style`}>
+          <InputNumberFiled
+            label="字号"
+            value={groupConfig.fontSize}
+            width={68}
+            onChange={(value) => {
+              onGroupConfigChange('fontSize', value);
+            }}
+          />
+          <ColorPicker
+            value={groupConfig.fontFill}
+            onChange={(value: string) => {
+              onGroupConfigChange('fontFill', value);
+            }}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export const GroupService: React.FC<any> = (props) => {
+  return (
+    <FlowchartFormWrapper {...props}>
+      {(config, plugin) => (
+        <GroupComponent {...props} plugin={plugin} config={config} />
+      )}
+    </FlowchartFormWrapper>
+  );
+};

+ 119 - 0
src/components/Flow/node/control-map-service/components/node.tsx

@@ -0,0 +1,119 @@
+import React, { useState, useEffect } from 'react';
+import { FlowchartFormWrapper } from '@antv/xflow';
+import {
+  InputFiled,
+  ColorPicker,
+  Position,
+  InputNumberFiled,
+  Size,
+} from '../../fields';
+import { PREFIX, DefaultNodeConfig } from '../../constants';
+
+export interface IConfig {
+  x?: number;
+  y?: number;
+  width?: number;
+  height?: number;
+  label?: string;
+  stroke?: string;
+  fill?: string;
+  fontSize?: number;
+  fontFill?: string;
+}
+
+const NodeComponent = (props: any) => {
+  const { config, plugin = {} } = props;
+  const { updateNode } = plugin;
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...DefaultNodeConfig,
+    ...config,
+  });
+  const onNodeConfigChange = (key: string, value: number | string) => {
+    setNodeConfig({
+      ...nodeConfig,
+      [key]: value,
+    });
+    updateNode({
+      [key]: value,
+    });
+  };
+
+  useEffect(() => {
+    setNodeConfig({
+      ...DefaultNodeConfig,
+      ...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);
+          }}
+        />
+        <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>
+      </div>
+    </div>
+  );
+};
+
+export const NodeService: React.FC<any> = (props) => {
+  return (
+    <FlowchartFormWrapper {...props}>
+      {(config, plugin) => (
+        <NodeComponent {...props} plugin={plugin} config={config} />
+      )}
+    </FlowchartFormWrapper>
+  );
+};

+ 138 - 0
src/components/Flow/node/control-map-service/components/style.less

@@ -0,0 +1,138 @@
+@prefix-cls: ~'flowchart-editor';
+
+.@{prefix-cls}-panel-body {
+  padding: 12px;
+
+  .@{prefix-cls}-color-container {
+    width: 24px;
+    height: 24px;
+    padding: 4px;
+    border: 1px solid #eee;
+    border-radius: 2px;
+  }
+
+  .@{prefix-cls}-panel-group {
+    display: flex;
+    flex-direction: column;
+    grid-gap: 8px;
+    margin-bottom: 12px;
+    padding-bottom: 12px;
+    font-size: 12px;
+
+    &:first-child {
+      border-bottom: 1px solid #ccc;
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    input,
+    select {
+      height: 24px;
+    }
+
+    h5 {
+      margin: 0;
+      color: rgba(0, 0, 0, 0.85);
+    }
+
+    .group {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      > label {
+        margin-right: 8px;
+        color: rgba(0, 0, 0, 0.45);
+        word-break: keep-all;
+      }
+    }
+    .split {
+      display: flex;
+      grid-gap: 8px;
+    }
+    .addon-before-group {
+      position: relative;
+      display: flex;
+      flex-direction: row;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+      vertical-align: center;
+      border: 1px solid #d9d9d9;
+      border-radius: 2px;
+
+      > span {
+        position: absolute;
+        top: 0;
+        right: 0;
+        display: block;
+        width: 20px;
+        color: #000000d9;
+        line-height: 24px;
+        text-align: center;
+        background-color: #fafafa;
+        cursor: pointer;
+      }
+
+      &:hover {
+        > span {
+          display: none;
+        }
+      }
+    }
+  }
+
+  .@{prefix-cls}-node-text-style,
+  .@{prefix-cls}-edge-text-style,
+  .@{prefix-cls}-edge-stroke-style {
+    display: flex;
+    flex-direction: row;
+    grid-gap: 8px;
+  }
+
+  .ant-input-number {
+    width: 100%;
+  }
+}
+
+.@{prefix-cls}-canvas-panel {
+  display: flex;
+  justify-content: center;
+  padding-top: 60px;
+  color: #aaa;
+}
+
+.@{prefix-cls}-pick-color-container {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 999;
+  background: rgba(0, 0, 0, 0.25);
+
+  .@{prefix-cls}-popover {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    padding: 12px;
+    background: #fff;
+    transform: translate(-50%, -50%);
+  }
+
+  .sketch-picker {
+    box-sizing: border-box !important;
+    padding: 0 !important;
+    border-radius: none !important;
+    box-shadow: none !important;
+  }
+
+  .foolter {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    margin-top: 12px;
+  }
+}

+ 18 - 0
src/components/Flow/node/control-map-service/index.ts

@@ -0,0 +1,18 @@
+import { NodeService } from './components/node';
+import { EdgeService } from './components/edge';
+import { GroupService } from './components/group';
+import { CanvasService } from './components/canvas';
+import { registerNode } from '../registerNode';
+
+/** 默认支持修改标签和重命名功能 */
+export default function controlMapService(controlMap: any) {
+  // 注册自定义组件
+  registerNode.map((node) => {
+    controlMap.set(`${node.name}`, node.service);
+  });
+  controlMap.set('node-service', NodeService);
+  controlMap.set('edge-service', EdgeService);
+  controlMap.set('group-service', GroupService);
+  controlMap.set('canvas-service', CanvasService);
+  return controlMap;
+}

+ 106 - 0
src/components/Flow/node/fields/color.tsx

@@ -0,0 +1,106 @@
+import React, { useState, useRef, memo } from 'react';
+import { render, createPortal } from 'react-dom';
+import  { IGraphConfig } from '@antv/xflow-core';
+import { useXFlowApp } from '@antv/xflow-core';
+import { Button } from 'antd';
+import { SketchPicker } from 'react-color';
+import { PREFIX } from '../constants';
+
+interface IProps {
+  label?: string;
+  value?: string;
+  onChange?: (value: string) => void;
+}
+
+const ColorPicker: React.FC<IProps> = (props) => {
+  const { label, value = '', onChange } = props;
+  const [show, setShow] = useState(false);
+  // const colorRef = useRef<string>(value);
+  const [color, setColor] = useState(value);
+  const { graphProvider } = useXFlowApp();
+  const graphConfig = useRef<IGraphConfig>();
+  graphProvider.getGraphOptions().then((x6GraphConfig) => {
+    graphConfig.current = x6GraphConfig;
+  });
+
+  const PickContainer = () => {
+    return (
+      <div className={`${PREFIX}-pick-color-container`}>
+        <div className={`${PREFIX}-popover`}>
+          <SketchPicker
+            onChangeComplete={(color: any) => {
+              // console.log(color);
+              setColor(color.hex);
+              // colorRef.current = color.hex;
+            }}
+            color={color}
+          />
+          <div className="foolter">
+            {value}
+            <Button
+              onClick={() => {
+                setShow(false);
+              }}
+            >
+              取消
+            </Button>
+            <Button
+              type="primary"
+              onClick={() => {
+                onChange?.(color);
+                // onChange?.(colorRef.current);
+                setShow(false);
+              }}
+            >
+              确认
+            </Button>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  const createPickColorContainer = (visible: boolean) => {
+    const existElements = document.getElementsByClassName(
+      `${PREFIX}-pick-color-container`,
+    );
+    if (existElements.length) {
+      Array.from(existElements).forEach((ele) => {
+        ele.parentNode?.removeChild(ele);
+      });
+    }
+    if (!visible) {
+      return;
+    }
+    const div = document.createElement('div');
+    render(
+      createPortal(<PickContainer />, document.getElementsByTagName('body')[0]),
+      div,
+    );
+  };
+
+  return (
+    <div className="group">
+      {label && <label>{label}</label>}
+      <div
+        className={`${PREFIX}-color-container`}
+        onClick={() => {
+          setShow(true);
+        }}
+      >
+        <div
+          className={`${PREFIX}-color`}
+          style={{
+            backgroundColor: value,
+            height: '100%',
+          }}
+        />
+      </div>
+      {createPickColorContainer(show)}
+    </div>
+  );
+};
+
+export default memo(ColorPicker, (pre, next) => {
+  return pre.label === next.label && pre.value === next.value;
+});

+ 92 - 0
src/components/Flow/node/fields/imgSelect.tsx

@@ -0,0 +1,92 @@
+import React, { useState } from 'react';
+import { Button, Modal, List, Image, Card, Tabs } from 'antd';
+
+const { TabPane } = Tabs;
+
+interface IImg {
+  type: string;
+  list?: Array<any>;
+}
+interface IProps {
+  value?: string;
+  imgList?: Array<IImg>;
+  onChange?: (value: any) => void;
+}
+
+const imgSelect: React.FC<IProps> = (props) => {
+  const { value, imgList, onChange } = props;
+  const [isModalVisible, setIsModalVisible] = useState(false);
+
+  const showModal = () => {
+    setIsModalVisible(true);
+  };
+
+  const handleOk = () => {
+    setIsModalVisible(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalVisible(false);
+  };
+  return (
+    <div className="group">
+      <Image src={value} />
+      <br />
+      <Button type="primary" onClick={showModal}>
+        替换
+      </Button>
+      <Modal
+        title="设备列表"
+        visible={isModalVisible}
+        footer={false}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <Tabs defaultActiveKey={imgList?.[0].type}>
+          {imgList?.map((img) => (
+            <TabPane tab={img.type} key={img.type}>
+              <List
+                dataSource={img.list}
+                grid={{
+                  gutter: 16,
+                  md: 4,
+                }}
+                renderItem={(item: any) => {
+                  let data: any;
+                  if (typeof item == 'object') {
+                    data = item;
+                  } else {
+                    data = {
+                      src: item,
+                    };
+                  }
+                  return (
+                    <List.Item>
+                      <Card>
+                        <div
+                          style={{
+                            width: 80,
+                            height: 80,
+                            background: `url(${data.src}) no-repeat center`,
+                            backgroundSize: 'contain',
+                            cursor: 'pointer',
+                          }}
+                          onClick={() => {
+                            onChange?.(data);
+                            handleCancel();
+                          }}
+                        ></div>
+                      </Card>
+                    </List.Item>
+                  );
+                }}
+              />
+            </TabPane>
+          ))}
+        </Tabs>
+      </Modal>
+    </div>
+  );
+};
+
+export default imgSelect;

+ 23 - 0
src/components/Flow/node/fields/index.ts

@@ -0,0 +1,23 @@
+import InputFiled from './input';
+import ColorPicker from './color';
+import InputNumberFiled from './input-number';
+import Size from './size';
+import Position from './position';
+import SelectField from './select';
+import RadioField from './radio';
+import PlcDevice from './plcDevice';
+import ImgSelect from './imgSelect';
+import UploadFiled from './upload';
+
+export {
+  InputFiled,
+  ColorPicker,
+  InputNumberFiled,
+  Size,
+  Position,
+  SelectField,
+  PlcDevice,
+  ImgSelect,
+  RadioField,
+  UploadFiled,
+};

+ 34 - 0
src/components/Flow/node/fields/input-number.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+import { InputNumber } from 'antd';
+import { FormItemHeight } from '../constants';
+
+interface IProps {
+  label?: string;
+  value?: number;
+  min?: number;
+  width?: number;
+  style?: object;
+  onChange?: (value: number) => void;
+}
+
+const InputNumberFiled: React.FC<IProps> = (props) => {
+  const { label, value, onChange, min, width ,style} = props;
+  return (
+    <div className="group" style={style}>
+      {label && <label>{label}</label>}
+      <InputNumber
+        value={value}
+        min={min}
+        style={{
+          width,
+          height: FormItemHeight,
+        }}
+        onChange={(v: number) => {
+          onChange?.(v);
+        }}
+      />
+    </div>
+  );
+};
+
+export default InputNumberFiled;

+ 31 - 0
src/components/Flow/node/fields/input.tsx

@@ -0,0 +1,31 @@
+import React from 'react';
+import { Input } from 'antd';
+import { FormItemHeight } from '../constants';
+
+interface IProps {
+  label?: string;
+  value?: string;
+  disabled?: boolean;
+  onChange?: (value: string) => void;
+}
+
+const InputFiled: React.FC<IProps> = props => {
+  const { label = '标签', value, onChange, disabled } = props;
+  return (
+    <div className="group">
+      <label>{label}</label>
+      <Input
+        value={value}
+        style={{
+          height: FormItemHeight,
+        }}
+        disabled={disabled}
+        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+          onChange?.(e.target.value);
+        }}
+      />
+    </div>
+  );
+};
+
+export default InputFiled;

+ 55 - 0
src/components/Flow/node/fields/plcDevice.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+import { Select } from 'antd';
+import { FormItemHeight } from '../constants';
+
+const { Option } = Select;
+
+interface IOption {
+  label?: string | Number;
+  value?: string | Number;
+}
+
+interface IProps {
+  label?: string;
+  value?: any;
+  // options?: IOption[];
+  onChange?: (value: string) => void;
+}
+
+const InputFiled: React.FC<IProps> = (props) => {
+  const { label = '标签', value, onChange } = props;
+  const options = [
+    {
+      label: 'plc1',
+      value: 1,
+    },
+    {
+      label: 'plc2',
+      value: 2,
+    },
+    {
+      label: 'plc3',
+      value: 3,
+    },
+  ];
+  return (
+    <div className="group">
+      <label>{label}</label>
+      <Select
+        value={value}
+        style={{
+          height: FormItemHeight,
+        }}
+        onChange={(value: any) => {
+          onChange?.(value);
+        }}
+      >
+        {options.map((item) => (
+          <Option value={item.value}>{item.label}</Option>
+        ))}
+      </Select>
+    </div>
+  );
+};
+
+export default InputFiled;

+ 50 - 0
src/components/Flow/node/fields/position.tsx

@@ -0,0 +1,50 @@
+import React from 'react';
+import { InputNumber } from 'antd';
+import { FormItemHeight } from '../constants';
+
+interface IProps {
+  x?: number;
+  y?: number;
+  onChange?: (key: string, value: number) => void;
+}
+
+export const Item = ({ value, onChangeItem, addonBefore }: any) => (
+  <div className="addon-before-group">
+    <InputNumber
+      value={value}
+      style={{ height: FormItemHeight, border: 'none' }}
+      onChange={(v: number) => {
+        onChangeItem(v);
+      }}
+    />
+    <span>{addonBefore}</span>
+  </div>
+);
+
+const Position: React.FC<IProps> = (props) => {
+  const { x, y, onChange } = props;
+
+  return (
+    <div className="group">
+      <label>位置</label>
+      <div className="split">
+        <Item
+          addonBefore="X"
+          value={x}
+          onChangeItem={(value: number) => {
+            onChange?.('x', value);
+          }}
+        />
+        <Item
+          addonBefore="Y"
+          value={y}
+          onChangeItem={(value: number) => {
+            onChange?.('y', value);
+          }}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default Position;

+ 43 - 0
src/components/Flow/node/fields/radio.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import { Radio } from 'antd';
+import { FormItemHeight } from '../constants';
+
+interface IProps {
+  label?: string;
+  value?: string | number;
+  options?: {
+    label: string | number;
+    value: string | number;
+  }[];
+  width?: number | string;
+  disabled?: boolean;
+  onChange?: (value: string | number) => void;
+}
+
+const SelectField: React.FC<IProps> = props => {
+  const { label = '', value, onChange, options = [], width, disabled } = props;
+  return (
+    <div className="group">
+      <label>{label}</label>
+      <Radio.Group
+        value={value}
+        style={{
+          width,
+          height: FormItemHeight,
+        }}
+        disabled={disabled}
+        onChange={e => {
+          onChange?.(e.target.value);
+        }}
+      >
+        {options.map(item => (
+          <Radio key={`${item.value}-${item.label}`} value={item.value}>
+            {item.label}
+          </Radio>
+        ))}
+      </Radio.Group>
+    </div>
+  );
+};
+
+export default SelectField;

+ 48 - 0
src/components/Flow/node/fields/select.tsx

@@ -0,0 +1,48 @@
+import React from 'react';
+import { Select } from 'antd';
+import { FormItemHeight } from '../constants';
+
+interface IProps {
+  label?: string;
+  value?: string | number;
+  options?: {
+    label: string | number;
+    value: string | number;
+  }[];
+  width?: number | string;
+  disabled?: boolean;
+  onChange?: (value: string | number) => void;
+}
+
+const SelectField: React.FC<IProps> = props => {
+  const { label = '箭头', value, onChange, options = [], width, disabled } = props;
+  return (
+    <div className="group">
+      <label>{label}</label>
+      <Select
+        size="small"
+        value={value}
+        style={{
+          width,
+          height: FormItemHeight,
+        }}
+        disabled={disabled}
+        getPopupContainer={trigger => trigger.parentNode}
+        optionFilterProp="children"
+        onChange={(v: string) => {
+          onChange?.(v);
+        }}
+        filterOption={(input, option: any) => {
+          const { label: text = '' } = option;
+          if (typeof text === 'string') {
+            return text.toLowerCase().indexOf(input.toLowerCase()) >= 0;
+          }
+          return text.toString().indexOf(input.toLowerCase()) >= 0;
+        }}
+        options={options}
+      />
+    </div>
+  );
+};
+
+export default SelectField;

+ 36 - 0
src/components/Flow/node/fields/size.tsx

@@ -0,0 +1,36 @@
+import React from 'react';
+import { Item } from './position';
+
+interface IProps {
+  width?: number;
+  height?: number;
+  onChange?: (key: string, value: number) => void;
+}
+
+const Size: React.FC<IProps> = (props) => {
+  const { width, height, onChange } = props;
+
+  return (
+    <div className="group">
+      <label>尺寸</label>
+      <div className="split">
+        <Item
+          addonBefore="W"
+          value={width}
+          onChangeItem={(value: number) => {
+            onChange?.('width', value);
+          }}
+        />
+        <Item
+          addonBefore="H"
+          value={height}
+          onChangeItem={(value: number) => {
+            onChange?.('height', value);
+          }}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default Size;

+ 39 - 0
src/components/Flow/node/fields/upload.tsx

@@ -0,0 +1,39 @@
+import React, { useState, useEffect } from 'react';
+import { connect } from "umi";
+import AliyunOssUploader from '@/components/OssUpload/AliyunOssUploader';
+
+interface IProps {
+  label?: string;
+  value?: string;
+  OSSData?: any;
+  onChange?: (value: string) => void;
+  beforeUpload?: (file: any) => Boolean;
+  onUploading?: (value: string) => void;
+}
+
+const UploadFiled: React.FC<IProps> = props => {
+  const { label = '标签', value, onChange, onUploading, OSSData, beforeUpload } = props;
+
+  const uploadProps = {
+    OSSData: OSSData,
+    onDone: file => {
+      let url: string = OSSData.host + '/' + file.url;
+      onChange(url);
+    },
+    onUploading: onUploading,
+    beforeUpload: beforeUpload,
+    noStyle: false,
+    showUploadList: false,
+  };
+
+  return (
+    <div className="group">
+      <label>{label}</label>
+      <AliyunOssUploader {...uploadProps} directory={false} label="上传"></AliyunOssUploader>
+    </div>
+  );
+};
+
+export default connect(({ xflow }) => ({
+  OSSData: xflow.OSSData,
+}))(UploadFiled);

+ 104 - 0
src/components/Flow/node/formSchemaService.ts

@@ -0,0 +1,104 @@
+import { registerNode } from './registerNode';
+
+export default async function formSchemaService(args: any) {
+  const { targetType, targetData } = args;
+  const isGroup = targetData?.isGroup;
+  const groupSchema = {
+    tabs: [
+      {
+        name: '设置',
+        groups: [
+          {
+            name: 'groupName',
+            controls: [
+              {
+                label: '分组名',
+                name: 'group-service',
+                shape: 'group-service',
+                placeholder: '分组名称',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  };
+
+  const edgeSchema = {
+    tabs: [
+      {
+        name: '设置',
+        groups: [
+          {
+            name: 'groupName',
+            controls: [
+              {
+                label: '边',
+                name: 'edge-service',
+                shape: 'edge-service',
+                placeholder: '边名称',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  };
+
+  if (isGroup) {
+    return groupSchema;
+  }
+  if (targetType === 'edge') {
+    return edgeSchema;
+  }
+  if (targetType === 'node') {
+    // console.log(targetData);
+
+    return getNodeSchema(targetData.name);
+  }
+  return {
+    tabs: [
+      {
+        name: '设置',
+        groups: [
+          {
+            name: 'groupName',
+            controls: [
+              {
+                label: '',
+                name: 'canvas-service',
+                shape: 'canvas-service',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  };
+}
+
+const getNodeSchema: any = (shapeName: String) => {
+  // 判断是否为自定义节点
+  let node = registerNode.find((node) => node.name == shapeName);
+  let name = node?.name || 'node-service';
+  return {
+    tabs: [
+      {
+        name: '设置',
+        groups: [
+          {
+            name: 'groupName',
+            controls: [
+              {
+                label: '节点名',
+                name: name,
+                shape: name,
+                placeholder: '节点名称',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  };
+};

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

@@ -0,0 +1,122 @@
+import React, { useEffect, useMemo } from 'react';
+judgeServe;
+import judgeServe, { ComponentName, FormItem } from './mapServe';
+import {
+  JudgeType,
+  JudgeOptions,
+  SiginOptions,
+  SiginSmallOptions,
+} from '../../components/judgeComponent';
+import { connect } from "umi";
+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;
+              const getSmallSigin = (sigin: Number) =>
+                SiginSmallOptions.find(item => item.value == sigin)?.label;
+
+              text.push(
+                `${smallValue} ${getSmallSigin(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 "umi";
+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
+);

+ 43 - 0
src/components/Flow/node/rect/index.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import { message, Badge } from 'antd';
+import { useXFlowApp, XFlowNodeCommands } from '@antv/xflow';
+
+import RectServe from './mapServe';
+export { RectServe };
+
+export default function CustomRect(props) {
+  const { size = { width: 120, height: 50 }, data } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize } = data;
+  const app = useXFlowApp();
+
+  const handleClick = () => {
+    // console.log(data);
+    app.executeCommand(XFlowNodeCommands.SELECT_NODE.id, {
+      nodeId: data.id,
+    });
+    // console.log('XFlowNodeCommands.SELECT_NODE.id', data);
+    // message.success(`${XFlowNodeCommands.SELECT_NODE.label}: 命令执行成功`);
+  };
+
+  return (
+    <Badge count={data.versions}>
+      <div
+        style={{
+          width,
+          height,
+          border: `2px solid ${stroke || '#ADDCFC'}`,
+          backgroundColor: fill || '#E6F6FD',
+          color: fontFill,
+          fontSize,
+          display: 'flex',
+          justifyContent: 'center',
+          alignItems: 'center',
+        }}
+        onClick={handleClick}
+      >
+        <span>{label}</span>
+      </div>
+    </Badge>
+  );
+}

+ 291 - 0
src/components/Flow/node/rect/mapServe.tsx

@@ -0,0 +1,291 @@
+import React, { useState, useEffect } from 'react';
+import { FlowchartFormWrapper, MODELS, useXFlowApp } from '@antv/xflow';
+import { Button, message, Select } from 'antd';
+import {
+  Position,
+  Size,
+  ColorPicker,
+  InputNumberFiled,
+  InputFiled,
+  UploadFiled,
+  RadioField,
+} from '../fields';
+import { PREFIX } from '../constants';
+import { UnityAction } from '@/utils/utils';
+import { connect } from "umi";
+import { IProps } from '@/components/Flow/index';
+
+interface ExcelInfo {
+  file_name?: string;
+  excel_cols?: any;
+}
+
+export interface IConfig {
+  id?: string;
+  label?: string;
+  x?: number;
+  y?: number;
+  width?: number;
+  height?: number;
+  fontSize?: number;
+  count?: number;
+  fontFill?: string;
+  fill?: string;
+  stroke?: string;
+  muti_version?: string | number;
+  is_start_node?: string | number;
+  bom_template?: string;
+  version_name?: string;
+  data?: any;
+  excel_info?: ExcelInfo;
+  role_list?: string;
+  is_seal?: string | number;
+}
+
+const defaultConfig: IConfig = {
+  muti_version: 1,
+  is_start_node: 0,
+  is_seal: 0,
+  excel_info: { file_name: '' },
+};
+
+const Component = (props: any) => {
+  const { config, plugin = {}, roleList } = props;
+  const { updateNode } = plugin;
+  const [options, setOptions] = useState([]);
+  const [fileName, setFileName] = useState('');
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...defaultConfig,
+    ...config,
+  });
+
+  const app = useXFlowApp();
+  const [meta, setMeta] = useState<IProps['meta']>(null);
+  const onNodeConfigChange = (key: string, value: number | string | object) => {
+    if (key) {
+      setNodeConfig({
+        ...nodeConfig,
+        [key]: value,
+      });
+      updateNode({
+        [key]: value,
+      });
+    } else if (value instanceof Object) {
+      setNodeConfig({
+        ...nodeConfig,
+        ...value,
+      });
+      updateNode({
+        ...value,
+      });
+    }
+  };
+
+  const onSave = () => {
+    UnityAction.emit('NODE_SAVE', nodeConfig);
+  };
+
+  useEffect(() => {
+    if(config.id != nodeConfig.id) {
+      setNodeConfig({
+        ...defaultConfig,
+        ...config,
+      });
+    }
+  }, [config]);
+
+  useEffect(() => {
+    MODELS.GRAPH_META.useValue(app.modelService).then((meta: IProps['meta']) => {
+      setMeta(meta);
+    });
+  }, []);
+
+  const updataFileName = (name: string) => {
+    var idx = name?.lastIndexOf('/');
+    let str = name.substring(idx + 1, name.length);
+    setFileName(str);
+    onNodeConfigChange('version_name', 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`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={nodeConfig.label}
+          disabled={meta?.editMode == 2}
+          onChange={value => {
+            onNodeConfigChange('label', value);
+          }}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>数据</h5>
+        <RadioField
+          label="多个版本"
+          value={nodeConfig.muti_version}
+          onChange={value => {
+            onNodeConfigChange('muti_version', value);
+          }}
+          options={[
+            { label: '是', value: 1 },
+            { label: '否', value: 0 },
+          ]}
+        />
+        <RadioField
+          label="起始起点"
+          value={nodeConfig.is_start_node}
+          onChange={value => {
+            onNodeConfigChange('is_start_node', value);
+          }}
+          options={[
+            { label: '是', value: 1 },
+            { label: '否', value: 0 },
+          ]}
+        />
+        <RadioField
+          label="是否用印"
+          value={nodeConfig.is_seal}
+          onChange={value => {
+            onNodeConfigChange('is_seal', value);
+          }}
+          options={[
+            { label: '是', value: 1 },
+            { label: '否', value: 0 },
+          ]}
+        />
+        {nodeConfig.is_start_node == 1 && (
+          <>
+            <InputFiled
+              label="模板名称"
+              value={nodeConfig.version_name}
+              onChange={value => {
+                onNodeConfigChange('version_name', value);
+              }}
+            />
+
+            {/* <UploadFiled
+              label="模板"
+              onChange={url => onNodeConfigChange('bom_template', url)}
+              beforeUpload={beforeUpload}
+            /> */}
+
+            {/* <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(','));
+            }}
+            disabled={meta?.editMode == 2}
+            options={options}
+          />
+        </div>
+      </div>
+      {meta?.editMode != 2 && (
+        <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 style={{ display: 'flex' }}>
+            <InputNumberFiled
+              label="字号"
+              value={nodeConfig.fontSize}
+              width={68}
+              onChange={value => {
+                onNodeConfigChange('fontSize', value);
+              }}
+              style={{ marginRight: 10 }}
+            />
+            <ColorPicker
+              value={nodeConfig.fontFill}
+              onChange={(value: string) => {
+                onNodeConfigChange('fontFill', value);
+              }}
+            />
+          </div>
+        </div>
+      )}
+      <Button 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(({ user }) => ({ roleList: user.roleList }))(RecthServe);

+ 45 - 0
src/components/Flow/node/registerNode.tsx

@@ -0,0 +1,45 @@
+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>,
+    name: 'custom-rect',
+    width: 120,
+    height: 50,
+    label: '业务节点',
+    service: RectServe,
+  },
+  {
+    component: Circle,
+    popover: () => <div>审批节点</div>,
+    name: 'custom-circle',
+    width: 90,
+    height: 90,
+    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,
+  },
+];

+ 34 - 0
src/components/Flow/node/utils.ts

@@ -0,0 +1,34 @@
+import { uuidv4 } from '@antv/xflow';
+
+export const getAnchorStyle = (position: string) => {
+  return {
+    position: { name: position },
+    attrs: {
+      circle: {
+        r: 4,
+        magnet: true,
+        stroke: '#31d0c6',
+        strokeWidth: 2,
+        fill: '#fff',
+        style: {
+          visibility: 'hidden',
+        },
+      },
+    },
+    zIndex: 10,
+  };
+};
+export const getPorts = (position = ['top', 'right', 'bottom', 'left']) => {
+  return {
+    items: position.map((name) => {
+      return { group: name, id: uuidv4() };
+    }),
+    groups: {
+      top: getAnchorStyle('top'),
+      right: getAnchorStyle('right'),
+      bottom: getAnchorStyle('bottom'),
+      left: getAnchorStyle('left'),
+      absolute: getAnchorStyle('absolute'),
+    },
+  };
+};

+ 28 - 0
src/components/Flow/react-node/CustomCircle.js

@@ -0,0 +1,28 @@
+import React from 'react';
+// import './dnd-node.less'
+
+export default function CustomRect(props) {
+  const { size = { width: 90, height: 90 }, data } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize } = data;
+
+  return (
+    <div
+      className="container"
+      style={{
+        width,
+        height,
+        border: `2px solid ${stroke || '#FEDDAE'}`,
+        backgroundColor: fill || '#FEF7E7',
+        color: fontFill,
+        fontSize,
+        borderRadius: '50%',
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems:'center'
+      }}
+    >
+      <span>{label}</span>
+    </div>
+  );
+}

+ 41 - 0
src/components/Flow/react-node/CustomRect.js

@@ -0,0 +1,41 @@
+import React from 'react';
+import { message, Badge } from 'antd';
+import { useXFlowApp, XFlowNodeCommands } from '@antv/xflow';
+// import './dnd-node.less'
+
+export default function CustomRect(props) {
+  const { size = { width: 120, height: 50 }, data } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize } = data;
+  const app = useXFlowApp();
+
+  const handleClick = () => {
+    // console.log(data);
+    app.executeCommand(XFlowNodeCommands.SELECT_NODE.id, {
+      nodeId: data.id,
+    });
+    // console.log('XFlowNodeCommands.SELECT_NODE.id', data);
+    // message.success(`${XFlowNodeCommands.SELECT_NODE.label}: 命令执行成功`);
+  };
+
+  return (
+    <Badge count={data.count}>
+      <div
+        style={{
+          width,
+          height,
+          border: `2px solid ${stroke || '#ADDCFC'}`,
+          backgroundColor: fill || '#E6F6FD',
+          color: fontFill,
+          fontSize,
+          display: 'flex',
+          justifyContent: 'center',
+          alignItems: 'center',
+        }}
+        onClick={handleClick}
+      >
+        <span>{label}</span>
+      </div>
+    </Badge>
+  );
+}

+ 24 - 0
src/components/Flow/react-node/dnd-node.js

@@ -0,0 +1,24 @@
+import React from 'react'
+// import './dnd-node.less'
+
+export const DndNode = props => {
+  const { size = { width: 126, height: 104 }, data } = props
+  const { width, height } = size
+  const { label, stroke, fill, fontFill, fontSize } = data
+
+  return (
+    <div
+      className="container"
+      style={{
+        width,
+        height,
+        borderColor: stroke,
+        backgroundColor: fill,
+        color: fontFill,
+        fontSize,
+      }}
+    >
+      <span>{label}</span>
+    </div>
+  )
+}

+ 50 - 0
src/components/Flow/service.ts

@@ -0,0 +1,50 @@
+import { DND_RENDER_ID, NODE_WIDTH, NODE_HEIGHT } from './constant';
+import { NsGraph } from '@antv/xflow';
+import { NsGraphCmd } from '@antv/xflow';
+import { queryBoomFlowDetail } from '@/services/boom';
+
+/** mock 后端接口调用 */
+export namespace MockApi {
+  export const NODE_COMMON_PROPS = {
+    renderKey: DND_RENDER_ID,
+    width: NODE_WIDTH,
+    height: NODE_HEIGHT,
+  } as const;
+
+  /** 查图的meta元信息 */
+  export const queryGraphMeta: NsGraphCmd.GraphMeta.IArgs['graphMetaService'] = async (
+    args: any
+  ) => {
+    // console.log('queryMeta', args);
+    return { ...args, flowId: args.meta.flowId };
+  };
+  /** 加载图数据的api */
+  export const loadGraphData = async (meta: NsGraph.IGraphMeta) => {
+    const { data } = await queryBoomFlowDetail({ id: meta.flowId });
+
+    
+    // graphData.nodes = graphData.nodes.map(item => {
+    //   item.ports.groups = groups;
+    //   return item;
+    // });
+    // // graphData.edges = []
+    // graphData.edges = graphData.edges.map(item => {
+    //   item.attrs = attrs;
+    //   return item;
+    // });
+
+    return {
+      // nodes,
+      // edges,
+    };
+  };
+  /** 保存图数据的api */
+  export const saveGraphData: NsGraphCmd.SaveGraphData.IArgs['saveGraphDataService'] = async (
+    meta: NsGraph.IGraphMeta,
+    graphData: NsGraph.IGraphData
+  ) => {
+    console.log('saveGraphData api', meta, graphData);
+    localStorage.graphData = JSON.stringify(graphData);
+    return null;
+  };
+}

+ 4 - 0
src/components/Guide/Guide.less

@@ -0,0 +1,4 @@
+.title {
+  margin: 0 auto;
+  font-weight: 200;
+}

+ 23 - 0
src/components/Guide/Guide.tsx

@@ -0,0 +1,23 @@
+import { Layout, Row, Typography } from 'antd';
+import React from 'react';
+import styles from './Guide.less';
+
+interface Props {
+  name: string;
+}
+
+// 脚手架示例组件
+const Guide: React.FC<Props> = (props) => {
+  const { name } = props;
+  return (
+    <Layout>
+      <Row>
+        <Typography.Title level={3} className={styles.title}>
+          欢迎使用 <strong>{name}</strong> !
+        </Typography.Title>
+      </Row>
+    </Layout>
+  );
+};
+
+export default Guide;

+ 2 - 0
src/components/Guide/index.ts

@@ -0,0 +1,2 @@
+import Guide from './Guide';
+export default Guide;

+ 107 - 0
src/components/OssUpload/AliyunOssUploader.js

@@ -0,0 +1,107 @@
+import React from 'react';
+import { Upload, message, Button } from 'antd';
+
+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 file => {
+    const { OSSData } = this.state;
+    const expire = OSSData.expire * 1000;
+    file.url = OSSData.dir + '/' + file.name;
+    console.log(file);
+
+    if (expire < Date.now()) {
+      await this.init();
+    }
+    if (this.props.beforeUpload) {
+      return this.props.beforeUpload(file);
+    }
+
+    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">{label}</Button>}
+      </Upload>
+    );
+  }
+}
+
+export default AliyunOSSUpload;

+ 1 - 0
src/constants/index.ts

@@ -0,0 +1 @@
+export const DEFAULT_NAME = 'Umi Max';

+ 0 - 10
src/layouts/index.less

@@ -1,10 +0,0 @@
-.navs {
-  ul {
-    padding: 0;
-    list-style: none;
-    display: flex;
-  }
-  li {
-    margin-right: 1em;
-  }
-}

+ 0 - 10
src/layouts/index.tsx

@@ -1,10 +0,0 @@
-import { Link, Outlet } from 'umi';
-import styles from './index.less';
-
-export default function Layout() {
-  return (
-    <div className={styles.navs}>
-
-    </div>
-  );
-}

+ 13 - 0
src/models/global.ts

@@ -0,0 +1,13 @@
+// 全局共享数据示例
+import { DEFAULT_NAME } from '@/constants';
+import { useState } from 'react';
+
+const useUser = () => {
+  const [name, setName] = useState<string>(DEFAULT_NAME);
+  return {
+    name,
+    setName,
+  };
+};
+
+export default useUser;

+ 21 - 0
src/pages/Access/index.tsx

@@ -0,0 +1,21 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { Access, useAccess } from '@umijs/max';
+import { Button } from 'antd';
+
+const AccessPage: React.FC = () => {
+  const access = useAccess();
+  return (
+    <PageContainer
+      ghost
+      header={{
+        title: '权限示例',
+      }}
+    >
+      <Access accessible={access.canSeeAdmin}>
+        <Button>只有 Admin 可以看到这个按钮</Button>
+      </Access>
+    </PageContainer>
+  );
+};
+
+export default AccessPage;

+ 128 - 0
src/pages/Flow/Audit.js

@@ -0,0 +1,128 @@
+import React, { useEffect, useRef, useMemo } from 'react';
+import { Button, Tabs } from 'antd';
+import { connect } from "umi";
+import Flow from '@/components/Flow';
+import AuditForm from '@/components/AuditForm';
+const { TabPane } = Tabs;
+
+const FLOWID = 2;
+
+function Audit(props) {
+  const {
+    roleList,
+    currentItem,
+    dispatch,
+    formItems,
+    formData,
+    flowDetail,
+    simpleFlowDteail,
+    currentUser,
+    loading,
+  } = props;
+  const ref = useRef();
+  const permission = currentUser.Permission;
+
+  const curItem = useMemo(() => {
+    let item = localStorage.getItem('currentAudit');
+    return JSON.stringify(currentItem) == '{}' ? JSON.parse(item) : currentItem;
+  }, [currentItem, localStorage.getItem('currentAudit')]);
+
+  const editMode = useMemo(() => {
+    // 判断是否有权限
+    if (permission['func-01-point-bom-flow']) {
+      return 1;
+    }
+    // 判断是否为创建者
+    if (currentUser.IsSuper) {
+      return 1;
+    }
+
+    return 2;
+  }, [permission, flowDetail]);
+
+  useEffect(() => {
+    dispatch({
+      type: 'flow/queryProcessFlows',
+      payload: { ids: Number(curItem.id) },
+    });
+    dispatch({
+      type: 'user/getRoleList',
+    });
+    dispatch({
+      type: 'user/fetch',
+    });
+
+    dispatch({
+      type: 'user/fetchDepV2',
+    });
+  }, []);
+
+  const onChange = values => {
+    dispatch({
+      type: 'xflow/save',
+      payload: {
+        formData: values,
+      },
+    });
+  };
+
+  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 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', editMode, flowId: curItem.id }}
+            flowDetail={flowDetail}
+            ref={ref}
+          />
+        </TabPane>
+      </Tabs>
+      <Button
+        loading={loading['flow/saveAuditFlowInfo']}
+        type="primary"
+        onClick={handleSaveClick}
+        style={{ position: 'absolute', right: 0, top: 0 }}
+      >
+        保存
+      </Button>
+    </div>
+  );
+}
+export default connect(({ flow, loading, user, xflow }) => ({
+  roleList: flow.roleList,
+  currentItem: flow.current,
+  loading: loading.effects,
+  formItems: xflow.formData,
+  flowDetail: flow.flowDetail,
+  formData: flow.formData,
+  currentUser: user.currentUser,
+  simpleFlowDteail: flow.simpleFlowDteail,
+}))(Audit);

+ 16 - 0
src/pages/Flow/Audit.less

@@ -0,0 +1,16 @@
+.box {
+  margin-bottom: 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.btns {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  :global {
+    .ant-btn {
+      margin-right: 20px;
+    }
+  }
+}

+ 39 - 0
src/pages/Flow/AuditModal.js

@@ -0,0 +1,39 @@
+import React, { useEffect } from "react";
+import { Modal, Input, Form } from "antd";
+
+// 审批意见
+function AuditModal(props) {
+  const { visible, onCancel, onOk, loading } = props;
+  const [form] = Form.useForm();
+  const formLayout = { labelCol: { span: 4 }, wrapperCol: { span: 14 } };
+
+  const handleOk = async () => {
+    let fieldsValue = await form.validateFields();
+    onOk(fieldsValue);
+  };
+
+  useEffect(() => {
+    if (visible) form.resetFields();
+  }, [visible]);
+  return (
+    <Modal
+      confirmLoading={loading}
+      destroyOnClose
+      title="流程"
+      open={visible}
+      onCancel={onCancel}
+      onOk={handleOk}
+    >
+      <Form {...formLayout} form={form}>
+        <Form.Item label="流程名称" name="name">
+          <Input />
+        </Form.Item>
+        <Form.Item label="详情" name="desc">
+          <Input.TextArea />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+}
+
+export default AuditModal;

+ 101 - 0
src/pages/Flow/index.js

@@ -0,0 +1,101 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Button, Table } from 'antd';
+import { connect, useNavigate } from "umi";
+import AuditModal from './AuditModal';
+import styles from './Audit.less';
+
+
+function Audit(props) {
+  const {  list = [], dispatch, loading } = props;
+  let navigate = useNavigate();
+  const [form] = Form.useForm();
+  const [visible, setVisible] = useState({
+    audit: false,
+    auditNode: false,
+  });
+  const columns = [
+    {
+      title: '审批流名称',
+      dataIndex: ['list', 'name'],
+    },
+    {
+      title: '操作',
+      render: (item, index) => (
+        <>
+          <a
+            onClick={() => {
+              setCurrentNode(item);
+            }}
+          >
+            编辑
+          </a>
+        </>
+      ),
+    },
+  ];
+  const handleAuditOk = values => {
+    console.log(values);
+    dispatch({
+      type: 'flow/addAudit',
+      payload: values,
+      callback: () => {
+        changeVisible('audit', false);
+      },
+    });
+  };
+  const changeVisible = (type, visible) => {
+    setVisible({
+      ...visible,
+      [type]: visible,
+    });
+  };
+  const setCurrentNode = item => {
+    if (item?.list) localStorage.setItem('currentAudit', JSON.stringify(item.list));
+    dispatch({
+      type: 'flow/save',
+      payload: {
+        current: item.list,
+      },
+    });
+    navigate('/home/audit');
+  };
+
+  useEffect(() => {
+    dispatch({
+      type: 'flow/queryAuditList',
+    });
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.box}>
+        <Button onClick={() => navigate(-1)}>返回</Button>
+        <Form layout="inline" name="basic" autoComplete="off" form={form}>
+          <Form.Item>
+            <Button onClick={() => changeVisible('audit', true)} type="primary">
+              新建流程
+            </Button>
+          </Form.Item>
+        </Form>
+      </div>
+
+      <Table
+        loading={loading['flow/queryAuditList']}
+        rowKey="id"
+        dataSource={list}
+        columns={columns}
+      />
+      <AuditModal
+        loading={loading['flow/addAudit']}
+        visible={visible.audit}
+        onOk={handleAuditOk}
+        onCancel={() => changeVisible('audit', false)}
+      />
+    </div>
+  );
+}
+export default connect(({  flow, loading }) => ({
+  userList: [],
+  list: flow.auditList,
+  loading: loading.effects,
+}))(Audit);

+ 291 - 0
src/pages/Flow/models/flow.js

@@ -0,0 +1,291 @@
+import {
+  queryAuditList,
+  addAudit,
+  addAuditNode,
+  addFlow,
+  queryBoomFlowDetail,
+  updateNode,
+  queryFlowList,
+  queryDingTemplateList,
+  saveAuditFlowInfo,
+  queryDepV2,
+  queryProcessFlows,
+  queryDefaultBindClassify,
+} from '@/services/boom';
+import {
+  queryApproval,
+} from '@/services/approval';
+import { queryRole } from '@/services/SysAdmin';
+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: {
+    *addFlow({ payload, callback }, { call, put }) {
+      const res = yield call(addFlow, payload);
+      if (res) {
+        message.success('添加成功');
+        callback && callback();
+        yield put({
+          type: 'queryFlowList',
+        });
+      }
+    },
+    *getRoleList({ payload }, { call, put }) {
+      const response = yield call(queryRole, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { roleList: response.data.list },
+        });
+      }
+    },
+    *queryBoomFlowDetail({ payload }, { call, put }) {
+      const data = yield call(queryBoomFlowDetail, payload);
+      console.log(data);
+      yield put({
+        type: 'save',
+        payload: { flowDetail: data },
+      });
+    },
+    *queryProject({ callback }, { call, put }) {
+      const response = yield call(queryApproval,{pageSize: 99999});
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: {
+            projectList: response.data.list,
+          },
+        });
+      }
+    },
+    *queryFlowList({ payload }, { call, put }) {
+      const res = yield call(queryFlowList, payload);
+      yield put({
+        type: 'save',
+        payload: { flowList: res.data },
+      });
+    },
+    *updateNode({ payload, callback }, { call, put }) {
+      const data = yield call(updateNode, payload);
+      console.log(data);
+      message.success('修改成功');
+      callback && callback();
+    },
+    *queryAuditList({ payload }, { call, put }) {
+      const response = yield call(queryAuditList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { auditList: response.data },
+        });
+      }
+    },
+    *addAudit({ payload, callback }, { call, put }) {
+      const response = yield call(addAudit, payload);
+      if (response) {
+        message.success('新增成功');
+        callback && callback();
+        yield put({
+          type: 'queryAuditList',
+          payload: {},
+        });
+      }
+    },
+    *addAuditNode({ payload, callback }, { call, put }) {
+      const response = yield call(addAuditNode, payload);
+      if (response) {
+        message.success('操作成功');
+        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,
+          },
+        });
+      }
+    },
+    *queryDefaultBindClassify({ payload }, { call, put }) {
+      const data = yield call(queryDefaultBindClassify, payload);
+      if (data) {
+       
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 3 - 0
src/pages/Home/index.less

@@ -0,0 +1,3 @@
+.container {
+  padding-top: 80px;
+}

+ 18 - 0
src/pages/Home/index.tsx

@@ -0,0 +1,18 @@
+import Guide from '@/components/Guide';
+import { trim } from '@/utils/format';
+import { PageContainer } from '@ant-design/pro-components';
+import { useModel } from '@umijs/max';
+import styles from './index.less';
+
+const HomePage: React.FC = () => {
+  const { name } = useModel('global');
+  return (
+    <PageContainer ghost>
+      <div className={styles.container}>
+        <Guide name={trim(name)} />
+      </div>
+    </PageContainer>
+  );
+};
+
+export default HomePage;

+ 26 - 0
src/pages/Table/components/CreateForm.tsx

@@ -0,0 +1,26 @@
+import { Modal } from 'antd';
+import React, { PropsWithChildren } from 'react';
+
+interface CreateFormProps {
+  modalVisible: boolean;
+  onCancel: () => void;
+}
+
+const CreateForm: React.FC<PropsWithChildren<CreateFormProps>> = (props) => {
+  const { modalVisible, onCancel } = props;
+
+  return (
+    <Modal
+      destroyOnClose
+      title="新建"
+      width={420}
+      open={modalVisible}
+      onCancel={() => onCancel()}
+      footer={null}
+    >
+      {props.children}
+    </Modal>
+  );
+};
+
+export default CreateForm;

+ 138 - 0
src/pages/Table/components/UpdateForm.tsx

@@ -0,0 +1,138 @@
+import {
+  ProFormDateTimePicker,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  StepsForm,
+} from '@ant-design/pro-components';
+import { Modal } from 'antd';
+import React from 'react';
+
+export interface FormValueType extends Partial<API.UserInfo> {
+  target?: string;
+  template?: string;
+  type?: string;
+  time?: string;
+  frequency?: string;
+}
+
+export interface UpdateFormProps {
+  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+  onSubmit: (values: FormValueType) => Promise<void>;
+  updateModalVisible: boolean;
+  values: Partial<API.UserInfo>;
+}
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => (
+  <StepsForm
+    stepsProps={{
+      size: 'small',
+    }}
+    stepsFormRender={(dom, submitter) => {
+      return (
+        <Modal
+          width={640}
+          bodyStyle={{ padding: '32px 40px 48px' }}
+          destroyOnClose
+          title="规则配置"
+          open={props.updateModalVisible}
+          footer={submitter}
+          onCancel={() => props.onCancel()}
+        >
+          {dom}
+        </Modal>
+      );
+    }}
+    onFinish={props.onSubmit}
+  >
+    <StepsForm.StepForm
+      initialValues={{
+        name: props.values.name,
+        nickName: props.values.nickName,
+      }}
+      title="基本信息"
+    >
+      <ProFormText
+        width="md"
+        name="name"
+        label="规则名称"
+        rules={[{ required: true, message: '请输入规则名称!' }]}
+      />
+      <ProFormTextArea
+        name="desc"
+        width="md"
+        label="规则描述"
+        placeholder="请输入至少五个字符"
+        rules={[
+          { required: true, message: '请输入至少五个字符的规则描述!', min: 5 },
+        ]}
+      />
+    </StepsForm.StepForm>
+    <StepsForm.StepForm
+      initialValues={{
+        target: '0',
+        template: '0',
+      }}
+      title="配置规则属性"
+    >
+      <ProFormSelect
+        width="md"
+        name="target"
+        label="监控对象"
+        valueEnum={{
+          0: '表一',
+          1: '表二',
+        }}
+      />
+      <ProFormSelect
+        width="md"
+        name="template"
+        label="规则模板"
+        valueEnum={{
+          0: '规则模板一',
+          1: '规则模板二',
+        }}
+      />
+      <ProFormRadio.Group
+        name="type"
+        width="md"
+        label="规则类型"
+        options={[
+          {
+            value: '0',
+            label: '强',
+          },
+          {
+            value: '1',
+            label: '弱',
+          },
+        ]}
+      />
+    </StepsForm.StepForm>
+    <StepsForm.StepForm
+      initialValues={{
+        type: '1',
+        frequency: 'month',
+      }}
+      title="设定调度周期"
+    >
+      <ProFormDateTimePicker
+        name="time"
+        label="开始时间"
+        rules={[{ required: true, message: '请选择开始时间!' }]}
+      />
+      <ProFormSelect
+        name="frequency"
+        label="监控对象"
+        width="xs"
+        valueEnum={{
+          month: '月',
+          week: '周',
+        }}
+      />
+    </StepsForm.StepForm>
+  </StepsForm>
+);
+
+export default UpdateForm;

+ 270 - 0
src/pages/Table/index.tsx

@@ -0,0 +1,270 @@
+import services from '@/services/demo';
+import {
+  ActionType,
+  FooterToolbar,
+  PageContainer,
+  ProDescriptions,
+  ProDescriptionsItemProps,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Divider, Drawer, message } from 'antd';
+import React, { useRef, useState } from 'react';
+import CreateForm from './components/CreateForm';
+import UpdateForm, { FormValueType } from './components/UpdateForm';
+
+const { addUser, queryUserList, deleteUser, modifyUser } =
+  services.UserController;
+
+/**
+ * 添加节点
+ * @param fields
+ */
+const handleAdd = async (fields: API.UserInfo) => {
+  const hide = message.loading('正在添加');
+  try {
+    await addUser({ ...fields });
+    hide();
+    message.success('添加成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('添加失败请重试!');
+    return false;
+  }
+};
+
+/**
+ * 更新节点
+ * @param fields
+ */
+const handleUpdate = async (fields: FormValueType) => {
+  const hide = message.loading('正在配置');
+  try {
+    await modifyUser(
+      {
+        userId: fields.id || '',
+      },
+      {
+        name: fields.name || '',
+        nickName: fields.nickName || '',
+        email: fields.email || '',
+      },
+    );
+    hide();
+
+    message.success('配置成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('配置失败请重试!');
+    return false;
+  }
+};
+
+/**
+ *  删除节点
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.UserInfo[]) => {
+  const hide = message.loading('正在删除');
+  if (!selectedRows) return true;
+  try {
+    await deleteUser({
+      userId: selectedRows.find((row) => row.id)?.id || '',
+    });
+    hide();
+    message.success('删除成功,即将刷新');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('删除失败,请重试');
+    return false;
+  }
+};
+
+const TableList: React.FC<unknown> = () => {
+  const [createModalVisible, handleModalVisible] = useState<boolean>(false);
+  const [updateModalVisible, handleUpdateModalVisible] =
+    useState<boolean>(false);
+  const [stepFormValues, setStepFormValues] = useState({});
+  const actionRef = useRef<ActionType>();
+  const [row, setRow] = useState<API.UserInfo>();
+  const [selectedRowsState, setSelectedRows] = useState<API.UserInfo[]>([]);
+  const columns: ProDescriptionsItemProps<API.UserInfo>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      tip: '名称是唯一的 key',
+      formItemProps: {
+        rules: [
+          {
+            required: true,
+            message: '名称为必填项',
+          },
+        ],
+      },
+    },
+    {
+      title: '昵称',
+      dataIndex: 'nickName',
+      valueType: 'text',
+    },
+    {
+      title: '性别',
+      dataIndex: 'gender',
+      hideInForm: true,
+      valueEnum: {
+        0: { text: '男', status: 'MALE' },
+        1: { text: '女', status: 'FEMALE' },
+      },
+    },
+    {
+      title: '操作',
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <a
+            onClick={() => {
+              handleUpdateModalVisible(true);
+              setStepFormValues(record);
+            }}
+          >
+            配置
+          </a>
+          <Divider type="vertical" />
+          <a href="">订阅警报</a>
+        </>
+      ),
+    },
+  ];
+
+  return (
+    <PageContainer
+      header={{
+        title: 'CRUD 示例',
+      }}
+    >
+      <ProTable<API.UserInfo>
+        headerTitle="查询表格"
+        actionRef={actionRef}
+        rowKey="id"
+        search={{
+          labelWidth: 120,
+        }}
+        toolBarRender={() => [
+          <Button
+            key="1"
+            type="primary"
+            onClick={() => handleModalVisible(true)}
+          >
+            新建
+          </Button>,
+        ]}
+        request={async (params, sorter, filter) => {
+          const { data, success } = await queryUserList({
+            ...params,
+            // FIXME: remove @ts-ignore
+            // @ts-ignore
+            sorter,
+            filter,
+          });
+          return {
+            data: data?.list || [],
+            success,
+          };
+        }}
+        columns={columns}
+        rowSelection={{
+          onChange: (_, selectedRows) => setSelectedRows(selectedRows),
+        }}
+      />
+      {selectedRowsState?.length > 0 && (
+        <FooterToolbar
+          extra={
+            <div>
+              已选择{' '}
+              <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
+              项&nbsp;&nbsp;
+            </div>
+          }
+        >
+          <Button
+            onClick={async () => {
+              await handleRemove(selectedRowsState);
+              setSelectedRows([]);
+              actionRef.current?.reloadAndRest?.();
+            }}
+          >
+            批量删除
+          </Button>
+          <Button type="primary">批量审批</Button>
+        </FooterToolbar>
+      )}
+      <CreateForm
+        onCancel={() => handleModalVisible(false)}
+        modalVisible={createModalVisible}
+      >
+        <ProTable<API.UserInfo, API.UserInfo>
+          onSubmit={async (value) => {
+            const success = await handleAdd(value);
+            if (success) {
+              handleModalVisible(false);
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          rowKey="id"
+          type="form"
+          columns={columns}
+        />
+      </CreateForm>
+      {stepFormValues && Object.keys(stepFormValues).length ? (
+        <UpdateForm
+          onSubmit={async (value) => {
+            const success = await handleUpdate(value);
+            if (success) {
+              handleUpdateModalVisible(false);
+              setStepFormValues({});
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          onCancel={() => {
+            handleUpdateModalVisible(false);
+            setStepFormValues({});
+          }}
+          updateModalVisible={updateModalVisible}
+          values={stepFormValues}
+        />
+      ) : null}
+
+      <Drawer
+        width={600}
+        open={!!row}
+        onClose={() => {
+          setRow(undefined);
+        }}
+        closable={false}
+      >
+        {row?.name && (
+          <ProDescriptions<API.UserInfo>
+            column={2}
+            title={row?.name}
+            request={async () => ({
+              data: row || {},
+            })}
+            params={{
+              id: row?.name,
+            }}
+            columns={columns}
+          />
+        )}
+      </Drawer>
+    </PageContainer>
+  );
+};
+
+export default TableList;

+ 0 - 10
src/pages/index.tsx

@@ -1,10 +0,0 @@
-
-
-export default function HomePage() {
-  console.log("[[[[[[[[[[[[[[[[[[[[[[", CompositionEvent)
-  return (
-    <div>
-      
-    </div>
-  );
-}

+ 235 - 0
src/services/SysAdmin.js

@@ -0,0 +1,235 @@
+import {request }from 'umi';
+import { stringify } from 'qs';
+
+export async function queryRes(params) {
+  return request(`/res?${stringify(params)}`);
+}
+
+export async function queryMenu() {
+  return request(`/menu`);
+}
+
+export async function queryProjectMenu(params) {
+  return request(`/api/v2/user/project/menu/${params.ID}`);
+}
+
+export async function updateMenu(param = {}) {
+  return request(`/menu/update`, {
+    method: 'PUT',
+    body: param,
+  });
+}
+/**
+ *
+ * @param {object} param
+ * @param {string} param.ParentCode 二级节点的编码
+ * @param {string} param.Code 要增加的编码
+ * @param {string} param.Name 要增加的名称
+ * @returns
+ */
+export async function checkMenu(param = {}) {
+  return request(`/check/menu/info?ParentCode=${param.ParentCode}&Name=${param.Name}`);
+}
+
+export async function addMenu(param) {
+  return request(`/menu/create`, {
+    method: 'POST',
+    body: param,
+  });
+}
+export async function removeMenu(param) {
+  return request(`/menu/delete/${param.ID}`, { method: 'DELETE' });
+}
+
+export async function addRoleRes(param) {
+  return request(`/role/res`, {
+    method: 'POST',
+    body: param,
+  });
+}
+
+export async function removeRes(params) {
+  return request(`/res/delete/${params.ID}`, { method: 'DELETE' });
+}
+
+export async function addRes(params) {
+  return request('/res/create', {
+    method: 'POST',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function updateRes(params = {}, currentPage, pageSize) {
+  return request(`/res/update?currentPage=${currentPage}&pageSize=${pageSize}`, {
+    method: 'PUT',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function queryRole(params) {
+  return request(`/role?${stringify(params)}`);
+}
+
+export async function queryProjectRole(params) {
+  return request(`/api/v2/project/roles/get?${stringify(params)}`);
+}
+
+export async function removeRole(params) {
+  return request(`/role/delete/${params.ID}`, { method: 'DELETE' });
+}
+
+export async function addRole(params) {
+  return request('/role/create', {
+    method: 'POST',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function updateRole(params = {}, currentPage, pageSize) {
+  return request(`/role/update?currentPage=${currentPage}&pageSize=${pageSize}`, {
+    method: 'PUT',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function queryUser(params) {
+  return request(`/user?${stringify(params)}`);
+}
+
+export async function queryUserV2(params) {
+  return request(`/api/v2/user?${stringify(params)}`);
+}
+
+export async function addUserRole(param) {
+  return request(`/user/role`, {
+    method: 'POST',
+    body: param,
+  });
+}
+
+export async function removeUser(params) {
+  return request(`/user/delete/${params.ID}`, { method: 'DELETE' });
+}
+
+export async function addUser(params) {
+  return request('/user/register', {
+    method: 'POST',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function updateUser(params = {}, currentPage, pageSize) {
+  return request(`/user/update?currentPage=${currentPage}&pageSize=${pageSize}`, {
+    method: 'PUT',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function queryDepRole(params) {
+  return request(`/api/v2/dep/role?${stringify(params)}`);
+}
+// 为部门设置角色
+export async function bindDepRole(params) {
+  return request(`/api/v2/dep/role`, {
+    method: 'POST',
+    body: params,
+  });
+}
+
+export async function queryUserDetail(user) {
+  return request(`/api/v2/user/detail/${user.ID}`);
+}
+// 为用户设置角色
+export async function setupDepRole(data) {
+  return request(`/api/v2/user/dep/role`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+export async function queryDep(params) {
+  return request(`/dep?${stringify(params)}`);
+}
+export async function queryDepV2(params) {
+  return request(`/api/v2/dep?${stringify(params)}`);
+}
+
+export async function queryDepUser(params) {
+  return request(`/api/v2/dep/user?${stringify(params)}`);
+}
+
+export async function removeDep(params) {
+  return request(`/dep/delete/${params.ID}`, {
+    method: 'DELETE',
+  });
+}
+
+export async function addDep(params) {
+  return request('/dep/create', {
+    method: 'POST',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function updateDep(params = {}, currentPage, pageSize) {
+  return request(`/dep/update?currentPage=${currentPage}&pageSize=${pageSize}`, {
+    method: 'PUT',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function queryRoleV2(params) {
+  return request(`/api/v2/project/role/get?${stringify(params)}`);
+}
+// 为项目设置roleType=1的角色
+export async function setupRoleV2(params) {
+  return request(`/api/v2/project/role/setup`, {
+    method: 'POST',
+    body: params,
+  });
+}
+// 为项目设置roleType=2,3,4的角色
+export async function setupRolesV2(params) {
+  return request(`/api/v2/project/roles/setup`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function setupMenuV2(params) {
+  return request(`/api/v2/project/menu/setup`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function menuMove(params = {}) {
+  return request(`/menu/move`, {
+    method: 'PUT',
+    body: params,
+  });
+}
+
+export async function queryFeedbackList(params = {}) {
+  return request(`/issue_feedback/list?${stringify(params)}`);
+}
+export async function updateFeedbackList(params = {}) {
+  return request(`/issue_feedback/status`, {
+    method: 'PUT',
+    body: params,
+  });
+}

+ 112 - 0
src/services/approval.js

@@ -0,0 +1,112 @@
+import {request }from 'umi';
+import { stringify } from 'qs';
+
+export async function queryType() {
+  return request(`/api/v2/approval/type/dic`);
+}
+
+export async function queryFlow() {
+  return request(`/api/v2/approval/flow`);
+}
+
+export async function queryIndustry() {
+  return request(`/api/v2/approval/industry/dic`);
+}
+
+// 提交立项
+export async function createApproval(data) {
+  return request(`/api/v2/approval/record`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+// 更新立项
+export async function updateApproval(data) {
+  return request(`/api/v2/approval/record`, {
+    method: 'PUT',
+    body: data,
+  });
+}
+
+// 删除立项
+export async function deleteApproval(data) {
+  return request(`/api/v2/approval/record/${data.id}`, {
+    method: 'DELETE',
+  });
+}
+
+// 审批
+export async function authApproval(data) {
+  return request(`/api/v2/approval/auth`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+// 审批列表查询:获取当前用户可审批的立项列表
+export async function queryAuth(data) {
+  return request(`/api/v2/approval/list/auth?${stringify(data)}`);
+}
+
+// 查询立项 id=&pageSize=&currentPage=&user_id=
+export async function queryApproval(data) {
+  return request(`/api/v2/approval/record?${stringify(data)}`);
+}
+
+// 提交审核
+export async function submitAudit(data) {
+  return request(`/api/v2/approval/audit/submit`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//获取部门结构
+export async function queryDepV2(params) {
+  return request(`/api/v2/dep?${stringify(params)}`);
+}
+
+//添加项目成员
+export async function addMember(data) {
+  return request(`/api/v2/project_code/user`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//获取项目成员
+export async function queryMember(params) {
+  return request(`/api/v2/project_code/user?${stringify(params)}`);
+}
+
+//转执行
+export async function startExecution(data) {
+  return request(`/api/v2/project_code/to_exe`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//转运营
+export async function startOperate(data) {
+  return request(`/api/v2/project_code/to_opt`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//转质保
+export async function startQuality(data) {
+  return request(`/api/v2/project_code/to_wty`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//移除成员
+export async function deleteMember(data) {
+  return request(`/api/v2/project_code/user/${data.project_code_id}/${data.user_id}`, {
+    method: 'DELETE',
+  });
+}

+ 473 - 0
src/services/boom.js

@@ -0,0 +1,473 @@
+import { message } from 'antd';
+import {request }from 'umi';
+import { stringify } from 'qs';
+
+/**
+  project_id
+  version_id	大版本id
+  template_id	
+  template_node_id	查询某流程和某节点下最新版本的数据记录
+  node_id	查询某审批流程和某审批节点下最新版本的数据记录
+ */
+export async function queryRecord(params) {
+  return request(`/purchase/record?${stringify(params)}`);
+}
+//删除excel中单个sheet页
+export async function queryDelSheetRecord(params) {
+  const response = await request(`/purchase/bom/del-purchase-excel-sheet?${stringify(params)}`);
+  if (response.code == 200) {
+    // message.success('删除成功');
+  }
+}
+
+// 查询全部工作流
+export async function queryFlowList(params) {
+  return request(`/purchase/bom/flows?${stringify(params)}`);
+}
+
+// 根据节点id查询所有version
+export async function queryVserionByNode(params, signal) {
+  return request(`/purchase/bom/flow/node?${stringify(params)}`, {
+    signal,
+  });
+}
+
+export async function commitSheet(params) {
+  return request(`/purchase/record`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function approve(params) {
+  return request(`/purchase/audit/status`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function queryAuthority(params) {
+  const depId = localStorage.depId;
+  return request(`/purchase/bom/user/excel/col?depId=${depId}`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function addBomComment(params) {
+  return request(`/purchase/comment`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function queryBomComment(params) {
+  return request(`/purchase/comment?${stringify(params)}`);
+}
+
+/**
+ * 提交流转
+  "id":3, 当前流转文档id,必填
+  "project_id":46, 所属项目id
+  "template_id":1, 所属模板id ,必填
+  "template_node_id":34,所属节点id,必填
+  "next_template_id":1,跳转的下级业务模板id,必填
+  "next_template_node_id":2,跳转的下级业务节点id,必填
+  "flow_id":1, 跳转的下级审核流程id , 如果不为空,则说明流转的是审核节点,下级业务节点为审核通过后进入的业务节点
+  "node_id":1,跳转的下级审核节点id
+  "desc":"流转描述"
+ */
+export async function submitNextNode(params) {
+  return request(`/purchase/next/node/submit`, {
+    method: 'POST',
+    body: params,
+  });
+}
+export async function advanceSubmitNextNode(params) {
+  return request(`/api/v1/purchase/next/node/advance-submit`, {
+    method: 'POST',
+    body: params,
+  });
+}
+
+export async function queryDetail(params) {
+  let response = await request(`/purchase/record?${stringify(params)}`);
+  let sheet = response.data;
+  sheet.data = JSON.parse(sheet.data || '[]');
+  sheet.data.forEach(item => {
+    item.config = JSON.parse(item.config || '{}');
+    item.celldata = JSON.parse(item.cell_data || '[]');
+    delete item.cell_data;
+  });
+  return sheet;
+}
+export async function queryHistoryDetail(params) {
+  return request(`/purchase/record/history/detail?${stringify(params)}`);
+}
+export async function queryHistoryList(params) {
+  return request(`/purchase/record/history?${stringify(params)}`);
+}
+
+export async function queryBoomFlowList(params) {
+  return request(`/purchase/bom/flows?${stringify(params)}`);
+}
+//请求历史版本
+export async function queryVersionsTree(params) {
+  return request(`/api/v1/purchase/record/version/tree?${stringify(params)}`);
+}
+//查询业务节点的审核记录
+export async function queryAuditExcel(params) {
+  return request(`/api/v1/purchase/audit/excel?${stringify(params)}`);
+}
+//查询审批节点的审核记录
+export async function queryAuditRecord(params) {
+  return request(`/api/v1/purchase/audit/record?${stringify(params)}`);
+}
+//查询表单数据接口
+export async function queryDingSchema(params) {
+  return request(`/api/v1/purchase/bom/ding/schema?${stringify(params)}`);
+}
+export async function queryDingInstanceDetail(params) {
+  let res = await request(`/api/v1/purchase/bom/ding/instance-detail`, {
+    method: 'POST',
+    body: params,
+  });
+  if (res.data.errcode != 0) {
+    message.error(res.data.errmsg);
+    throw new Error(res.data.errmsg);
+  }
+  return res;
+}
+export async function queryDingInstanceExecute(params) {
+  let res = await request(`/api/v1/purchase/bom/ding/instance-execute`, {
+    method: 'POST',
+    body: params,
+  });
+  if (res.data.errcode != 0) {
+    message.error('审批失败,请联系管理员。');
+    throw new Error(res.data.errmsg);
+  }
+  return res;
+}
+export async function queryListParentByUser(params) {
+  return request(`/api/v1/purchase/bom/ding/department/list-parent-by-user`, {
+    method: 'POST',
+    body: params,
+  });
+}
+/**
+ * 查看项目流程列表
+ * project_id
+ */
+export async function queryProjectRecord(params) {
+  return request(`/purchase/bom/project/record?${stringify(params)}`);
+}
+/** 查看版本列表
+ *  project_id		
+    template_id		流程id
+    template_node_id	流程节点id
+ */
+export async function queryVersionsList(params) {
+  return request(`/purchase/record/versions?${stringify(params)}`);
+}
+
+export async function queryBoomFlowDetail(params) {
+  let { data } = await request(`/purchase/bom/flow/info?${stringify(params)}`);
+  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,
+      id: item.node_id,
+      renderKey: item.render_key,
+      zIndex: item.z_index,
+      isCustom: !!item.is_custom,
+      ports: JSON.parse(item.ports || '{}'),
+    };
+    node.ports.groups = groups;
+    node.parentKey = '1';
+
+    return node;
+  });
+  let edges = data.Edges.map(item => {
+    let edge = {
+      id: item.edge_id,
+      source: {
+        cell: item.source_cell,
+        port: item.source_port,
+      },
+      target: {
+        cell: item.target_cell,
+        port: item.target_port,
+      },
+    };
+    try {
+      edge.attrs = item.attr ? JSON.parse(item.attr) : attrs;
+    } catch (error) {
+      edge.attrs = attrs;
+    }
+    return edge;
+  });
+  return {
+    ...data,
+    nodes,
+    edges,
+  };
+}
+export async function updateNode(data) {
+  return request(`/purchase/bom/flow/${data.templateId}/${data.nodeId}`, {
+    method: 'PUT',
+    body: data.body,
+  });
+}
+export async function addBoomFlow(data) {
+  return request(`/purchase/bom/flow/info`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+export async function queryAuditList(params) {
+  return request(`/purchase/flow/info?${stringify(params)}`);
+}
+
+export async function addAudit(data) {
+  return request(`/purchase/flow/info`, {
+    method: 'POST',
+    body: data,
+  });
+}
+export async function addFlow(data) {
+  return request(`/purchase/bom/flow/info`, {
+    method: 'POST',
+    body: data,
+  });
+}
+/**
+ *  [
+      {
+        "flow_id": 23,
+        "node": "主管",
+        "desc": "desc",
+        "auditor": 2,
+        "seq": 1,
+        "seq_relate": 0
+      }
+    ]
+ */
+export async function addAuditNode(data) {
+  return request(`/purchase/flow/info/${data.flowId}`, {
+    method: 'POST',
+    body: data.nodes,
+  });
+}
+
+export async function queryOSSData() {
+  return request(`/config/chart-template-img?destDir=public/bom`);
+}
+
+export async function queryRecordSheet(data) {
+  return request(`/purchase/record/sheet?${stringify(data)}`, {
+    method: 'POST',
+    body: data,
+  });
+}
+export async function queryDingTemplateList() {
+  return request(`/purchase/bom/ding/template/list`);
+}
+
+export async function queryDDdepList(data) {
+  let res = await request(`/api/v1/purchase/bom/ding/department-list`, {
+    method: 'POST',
+    body: data,
+  });
+  return res.data.result;
+}
+
+export async function queryDDProcessesForecast(data) {
+  let res = await request(`/api/v1/purchase/bom/ding/processes-forecast`, {
+    method: 'POST',
+    body: data,
+  });
+  if (res.data.message) {
+    // message.error(res.data.message);
+    throw new Error(res.data.message);
+  }
+  return res.data.result;
+}
+
+export async function uploadFile(data) {
+  let res = await request(`/api/v1/purchase/bom/ding/upload-file`, {
+    method: 'POST',
+    body: data,
+    headers: {
+      ContentType: 'application/x-www-form-urlencoded',
+    },
+  });
+  if (!res.data.dentry) {
+    message.error(res.data.errmsg);
+    throw new Error(res.data.errmsg);
+  }
+  return res.data;
+}
+
+export async function bindDDCode(userId, code) {
+  let res = await request(`/api/v1/purchase/bom/ding/set-ding-user-code?ucode=${userId}:${code}`, {
+    method: 'GET',
+  });
+
+  return res.data;
+}
+
+export async function saveAuditFlowInfo(data) {
+  return request(`/purchase/flow/info`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//获取部门结构
+export async function queryDepV2(params) {
+  return request(`/api/v2/dep?${stringify(params)}`);
+}
+
+export async function queryProcessFlows(params) {
+  let res = await request(`/purchase/process/get-flows?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res.data;
+}
+
+export async function queryUserListByRoleID(params) {
+  let res = await request(`/api/v1/purchase/process/get-role-user?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res.data;
+}
+
+//新增工作流时调用接口 给项目绑定默认分类列表
+//purchase/bom/default-bind-classify?project_id=1
+export async function queryDefaultBindClassify(params) {
+  let res = await request(`/purchase/bom/default-bind-classify?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res.data;
+}
+
+//获取分类列表
+export async function queryClassify() {
+  let res = await request(`/purchase/bom/get-classify`, {
+    method: 'GET',
+  });
+  return res.data;
+}
+
+export async function queryBindClassify(params) {
+  let res = await request(`/purchase/bom/get-bind-classify?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res.data;
+}
+
+export async function queryAddBindClassify(data) {
+  return request(`/purchase/bom/add-bind-classify`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+export async function queryDelPurchaseExcel(params) {
+  let res = await request(`/purchase/bom/del-purchase-excel?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res;
+}
+//提交流转存储表单审批人历史记录
+export async function querySaveBomForm(data) {
+  return request(`/purchase/bom/save-bom-form`, {
+    method: 'POST',
+    body: data,
+  });
+}
+export async function queryGetBomForm(params) {
+  let res = await request(`/purchase/bom/get-bom-form?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res;
+}
+
+//章管家失败,重新申请用印
+export async function queryTrySeal(params) {
+  let res = await request(`/purchase/bom/try-seal?${stringify(params)}`, {
+    method: 'GET',
+  });
+  return res;
+}
+
+export async function ChartTempOSSData(params) {
+  return request(`/purchase/bom/contract-file/${params.projectId}`);
+}
+// 设置最终版本
+export async function setLastVersion(excelId) {
+  return request(`/purchase/bom/set-last-version/${excelId}`);
+}

+ 96 - 0
src/services/demo/UserController.ts

@@ -0,0 +1,96 @@
+/* eslint-disable */
+// 该文件由 OneAPI 自动生成,请勿手动修改!
+import { request } from '@umijs/max';
+
+/** 此处后端没有提供注释 GET /api/v1/queryUserList */
+export async function queryUserList(
+  params: {
+    // query
+    /** keyword */
+    keyword?: string;
+    /** current */
+    current?: number;
+    /** pageSize */
+    pageSize?: number;
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.Result_PageInfo_UserInfo__>('/api/v1/queryUserList', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 POST /api/v1/user */
+export async function addUser(
+  body?: API.UserInfoVO,
+  options?: { [key: string]: any },
+) {
+  return request<API.Result_UserInfo_>('/api/v1/user', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 GET /api/v1/user/${param0} */
+export async function getUserDetail(
+  params: {
+    // path
+    /** userId */
+    userId?: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { userId: param0 } = params;
+  return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
+    method: 'GET',
+    params: { ...params },
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 PUT /api/v1/user/${param0} */
+export async function modifyUser(
+  params: {
+    // path
+    /** userId */
+    userId?: string;
+  },
+  body?: API.UserInfoVO,
+  options?: { [key: string]: any },
+) {
+  const { userId: param0 } = params;
+  return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    params: { ...params },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 DELETE /api/v1/user/${param0} */
+export async function deleteUser(
+  params: {
+    // path
+    /** userId */
+    userId?: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { userId: param0 } = params;
+  return request<API.Result_string_>(`/api/v1/user/${param0}`, {
+    method: 'DELETE',
+    params: { ...params },
+    ...(options || {}),
+  });
+}

+ 7 - 0
src/services/demo/index.ts

@@ -0,0 +1,7 @@
+/* eslint-disable */
+// 该文件由 OneAPI 自动生成,请勿手动修改!
+
+import * as UserController from './UserController';
+export default {
+  UserController,
+};

+ 68 - 0
src/services/demo/typings.d.ts

@@ -0,0 +1,68 @@
+/* eslint-disable */
+// 该文件由 OneAPI 自动生成,请勿手动修改!
+
+declare namespace API {
+  interface PageInfo {
+    /** 
+1 */
+    current?: number;
+    pageSize?: number;
+    total?: number;
+    list?: Array<Record<string, any>>;
+  }
+
+  interface PageInfo_UserInfo_ {
+    /** 
+1 */
+    current?: number;
+    pageSize?: number;
+    total?: number;
+    list?: Array<UserInfo>;
+  }
+
+  interface Result {
+    success?: boolean;
+    errorMessage?: string;
+    data?: Record<string, any>;
+  }
+
+  interface Result_PageInfo_UserInfo__ {
+    success?: boolean;
+    errorMessage?: string;
+    data?: PageInfo_UserInfo_;
+  }
+
+  interface Result_UserInfo_ {
+    success?: boolean;
+    errorMessage?: string;
+    data?: UserInfo;
+  }
+
+  interface Result_string_ {
+    success?: boolean;
+    errorMessage?: string;
+    data?: string;
+  }
+
+  type UserGenderEnum = 'MALE' | 'FEMALE';
+
+  interface UserInfo {
+    id?: string;
+    name?: string;
+    /** nick */
+    nickName?: string;
+    /** email */
+    email?: string;
+    gender?: UserGenderEnum;
+  }
+
+  interface UserInfoVO {
+    name?: string;
+    /** nick */
+    nickName?: string;
+    /** email */
+    email?: string;
+  }
+
+  type definitions_0 = null;
+}

+ 46 - 0
src/services/user.js

@@ -0,0 +1,46 @@
+import {request }from 'umi';
+
+export async function query() {
+  return request('/user');
+}
+
+export async function queryCurrent() {
+  return request('/user/current-user', { method: 'GET' });
+}
+
+export async function queryCurrentV2() {
+  return request('/api/v2/user/current-user', { method: 'GET' });
+}
+export async function queryUserRole(userId) {
+  return request('/api/v2/user/detail/' + userId, { method: 'GET' });
+}
+
+export async function queryUnreadNotification() {
+  return request('/notification/unread/', { method: 'GET' });
+}
+
+export async function SetNotificationRead(params) {
+  return request(`/notification/read/${params.ID}`, { method: 'PUT' });
+}
+
+// export async function queryUserList(param) {
+//   return request(`/userIdsAndNames?JWT-TOKEN=${param.token}`)
+// }
+
+export async function bindQywxUserId(param) {
+  return request(`/user/qywx-userId?code=${param.code}&JWT-TOKEN=${param.token}`);
+}
+
+export async function updateUser(param) {
+  return request(`/user/update`, {
+    method: 'PUT',
+    body: {
+      ...param,
+    },
+  });
+}
+
+//获取部门结构
+export async function queryDepV2(params) {
+  return request(`/api/v2/dep?${stringify(params)}`);
+}

+ 95 - 0
src/utils/event.js

@@ -0,0 +1,95 @@
+export class MessageEvent {
+  event = {};
+  on(type, callback) {
+    if (!this.event[type]) {
+      this.event[type] = [];
+    }
+    this.event[type].push(callback);
+  }
+  off(type, callback) {
+    if (callback) {
+      let index = this.event[type].indexOf(callback);
+      if (index == -1) return;
+      this.event[type].splice(index, 1);
+    } else {
+      this.event[type] = [];
+    }
+  }
+  emit(type, e) {
+    if (!this.event[type]) return;
+    this.event[type].forEach(item => {
+      item && item(e);
+    });
+  }
+}
+
+export class UnityMessageEvent extends MessageEvent {
+  constructor() {
+    super();
+    if (window.vuplex) {
+      window.vuplex.addEventListener('message', e => {
+        console.log('============================getMessageForUnity============================');
+        const data = JSON.parse(e.data);
+        console.log(data);
+        this.emit(data.type, data.message);
+
+        // 将消息广播给子页面
+        let iframeDom = document.getElementsByClassName('iframe');
+        for (let i = 0; i < iframeDom.length; i++) {
+          const item = iframeDom[i];
+          item.contentWindow.postMessage(data.type, data.message);
+        }
+      });
+    }
+  }
+  addEventListener(type, callback) {
+    if (window.vuplex) {
+      this.on(type, callback);
+    } else {
+      window[type] = callback;
+    }
+  }
+  sendMsg(type, message) {
+    if (window.parent) {
+      window.parent.postMessage({ type, message });
+    } else if (window.vuplex) {
+      window.vuplex.postMessage({ type, message });
+    }
+  }
+}
+
+export class PageMessageEvent extends MessageEvent {
+  constructor() {
+    super();
+    window.addEventListener('message', e => {
+      if (typeof e.data == 'string') {
+        try {
+          const data = JSON.parse(e.data);
+          if(data.type) {
+            this.emit(data.type, data.message);
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      }
+    });
+  }
+
+  sendMsg(type, message) {
+    let data = JSON.stringify({
+      type,
+      message,
+    });
+    if (window.parent) {
+      // 给父级发消息
+      window.parent.postMessage(data, '*');
+    } else {
+      // 给子集广播消息
+      let iframeDom = document.getElementsByClassName('iframe');
+      for (let i = 0; i < iframeDom.length; i++) {
+        const item = iframeDom[i];
+        item.contentWindow.postMessage(data, '*');
+      }
+    }
+  }
+}

+ 4 - 0
src/utils/format.ts

@@ -0,0 +1,4 @@
+// 示例方法,没有实际意义
+export function trim(str: string) {
+  return str.trim();
+}

+ 125 - 0
src/utils/request.js

@@ -0,0 +1,125 @@
+import { message,  } from 'antd';
+import { getToken, UnityAction } from './utils';
+
+/**
+ * Requests a URL, returning a promise.
+ *
+ * @param  {string} url       The URL we want to request
+ * @param  {object} [option] The options we want to pass to "fetch"
+ * @return {object}           An object containing either "data" or "err"
+ */
+
+//节流阀
+let flag = false;
+let tokenFlag = false;
+
+export default function request(url, option, jwt) {
+  const number = new Date().getTime();
+  // console.log(API_HOST);
+  const time = url.indexOf('?') > -1 ? `&time=${number}` : `?time=${number}`;
+  if (url.indexOf('http://') == -1 && url.indexOf('https://') == -1) {
+    if (url.indexOf('/api/') === -1) {
+      url = `/api/v1${url}${time}`;
+    }
+    // API_HOST在config/config.js的define中配置
+    // url = API_HOST + url
+  }
+  let token = getToken();
+
+  const options = {
+    ...option,
+  };
+
+  const defaultOptions = {
+    credentials: 'include',
+  };
+  const newOptions = { ...defaultOptions, ...options };
+
+  newOptions.headers = {
+    ...newOptions.headers,
+    'JWT-TOKEN': jwt ? jwt : `${token}`,
+  };
+  if (
+    newOptions.method === 'POST' ||
+    newOptions.method === 'PUT' ||
+    newOptions.method === 'DELETE'
+  ) {
+    if (!(newOptions.body instanceof FormData)) {
+      newOptions.headers = {
+        Accept: 'application/json',
+        'Content-Type': 'application/json;charset=utf-8',
+        ...newOptions.headers,
+      };
+      newOptions.body = JSON.stringify(newOptions.body);
+    } else {
+      // newOptions.body is FormData
+      newOptions.headers = {
+        Accept: 'application/json',
+        ...newOptions.headers,
+      };
+    }
+  }
+  
+  return (
+    fetch(url, newOptions)
+      .then(response => {
+        if (response.status === 204) {
+          return response.text();
+        }
+        return response.json();
+      })
+      .then(response => {
+        if (typeof response === 'string') {
+          return response;
+        }
+        //当http status正常但response里显示token超时则报错
+        else if ([401, 601, 602, 603].includes(response.code)) {
+          const error = new Error(response.data);
+          error.name = response.code;
+          error.response = response;
+          throw error;
+        } else if (response.code !== 200) {
+          if (flag) throw e;
+
+          flag = true;
+          setTimeout(() => {
+            flag = false;
+          }, 3000);
+          message.error(response.msg);
+          // return false;
+          throw new Error(response.msg);
+        } else {
+          return response;
+        }
+      })
+      .catch(e => {
+        const status = e.name;
+        if (status == 'AbortError') throw e;
+        // environment should not be used
+
+        if ([401, 601, 602, 603].includes(status)) {
+          if (tokenFlag) throw e;
+
+          tokenFlag = true;
+          setTimeout(() => {
+            tokenFlag = false;
+          }, 3000);
+          message.error('token失效,请重新登录');
+          // 在unity端无需跳转页面,  发消息给unity 跳转至登录页
+          UnityAction.sendMsg('sessionTimeout', {});
+          throw e;
+        }
+    
+
+        if (flag) throw e;
+
+        flag = true;
+        setTimeout(() => {
+          flag = false;
+        }, 3000);
+
+        message.error('网络连接错误,请重试');
+        throw e;
+      })
+  );
+}

+ 23 - 336
src/utils/utils.js

@@ -1,200 +1,4 @@
-import moment from 'moment';
-import React from 'react';
-import { notification, Icon } from 'antd';
-import nzh from 'nzh/cn';
-import { parse, stringify } from 'qs';
-import { SetNotificationRead } from '@/services/user';
-import request from '@/utils/request';
-import { MessageEvent, UnityMessageEvent, PageMessageEvent } from './event';
-
-export function fixedZero(val) {
-  return val * 1 < 10 ? `0${val}` : val;
-}
-
-export function getTimeDistance(type) {
-  const now = new Date();
-  const oneDay = 1000 * 60 * 60 * 24;
-
-  if (type === 'today') {
-    now.setHours(0);
-    now.setMinutes(0);
-    now.setSeconds(0);
-    return [moment(now), moment(now.getTime() + (oneDay - 1000))];
-  }
-
-  if (type === 'week') {
-    let day = now.getDay();
-    now.setHours(0);
-    now.setMinutes(0);
-    now.setSeconds(0);
-
-    if (day === 0) {
-      day = 6;
-    } else {
-      day -= 1;
-    }
-
-    const beginTime = now.getTime() - day * oneDay;
-
-    return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))];
-  }
-
-  if (type === 'month') {
-    const year = now.getFullYear();
-    const month = now.getMonth();
-    const nextDate = moment(now).add(1, 'months');
-    const nextYear = nextDate.year();
-    const nextMonth = nextDate.month();
-
-    return [
-      moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
-      moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000),
-    ];
-  }
-
-  const year = now.getFullYear();
-  return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
-}
-
-export function getPlainNode(nodeList, parentPath = '') {
-  const arr = [];
-  nodeList.forEach(node => {
-    const item = node;
-    item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
-    item.exact = true;
-    if (item.children && !item.component) {
-      arr.push(...getPlainNode(item.children, item.path));
-    } else {
-      if (item.children && item.component) {
-        item.exact = false;
-      }
-      arr.push(item);
-    }
-  });
-  return arr;
-}
-
-export function digitUppercase(n) {
-  return nzh.toMoney(n);
-}
-
-function getRelation(str1, str2) {
-  if (str1 === str2) {
-    console.warn('Two path are equal!'); // eslint-disable-line
-  }
-  const arr1 = str1.split('/');
-  const arr2 = str2.split('/');
-  if (arr2.every((item, index) => item === arr1[index])) {
-    return 1;
-  }
-  if (arr1.every((item, index) => item === arr2[index])) {
-    return 2;
-  }
-  return 3;
-}
-
-function getRenderArr(routes) {
-  let renderArr = [];
-  renderArr.push(routes[0]);
-  for (let i = 1; i < routes.length; i += 1) {
-    // 去重
-    renderArr = renderArr.filter(item => getRelation(item, routes[i]) !== 1);
-    // 是否包含
-    const isAdd = renderArr.every(item => getRelation(item, routes[i]) === 3);
-    if (isAdd) {
-      renderArr.push(routes[i]);
-    }
-  }
-  return renderArr;
-}
-
-/**
- * Get router routing configuration
- * { path:{name,...param}}=>Array<{name,path ...param}>
- * @param {string} path
- * @param {routerData} routerData
- */
-export function getRoutes(path, routerData) {
-  let routes = Object.keys(routerData).filter(
-    routePath => routePath.indexOf(path) === 0 && routePath !== path
-  );
-  // Replace path to '' eg. path='user' /user/name => name
-  routes = routes.map(item => item.replace(path, ''));
-  // Get the route to be rendered to remove the deep rendering
-  const renderArr = getRenderArr(routes);
-  // Conversion and stitching parameters
-  const renderRoutes = renderArr.map(item => {
-    const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
-    return {
-      exact,
-      ...routerData[`${path}${item}`],
-      key: `${path}${item}`,
-      path: `${path}${item}`,
-    };
-  });
-  return renderRoutes;
-}
-
-export function getPageQuery() {
-  return parse(window.location.href.split('?')[1]);
-}
-
-export function getQueryPath(path = '', query = {}) {
-  const search = stringify(query);
-  if (search.length) {
-    return `${path}?${search}`;
-  }
-  return path;
-}
-/* eslint no-useless-escape:0 */
-const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
-
-export function isUrl(path) {
-  return reg.test(path);
-}
-
-export function formatWan(val) {
-  const v = val * 1;
-  if (!v || Number.isNaN(v)) return '';
-
-  let result = val;
-  if (val > 10000) {
-    result = Math.floor(val / 10000);
-    result = (
-      <span>
-        {result}
-        <span
-          style={{
-            position: 'relative',
-            top: -2,
-            fontSize: 14,
-            fontStyle: 'normal',
-            marginLeft: 2,
-          }}
-        >
-          万
-        </span>
-      </span>
-    );
-  }
-  return result;
-}
-
-// 给官方演示站点用,用于关闭真实开发环境不需要使用的特性
-export function isAntdPro() {
-  return window.location.hostname === 'preview.pro.ant.design';
-}
-
-export const importCDN = (url, name) =>
-  new Promise(resolve => {
-    const dom = document.createElement('script');
-    dom.src = url;
-    dom.type = 'text/javascript';
-    dom.onload = () => {
-      resolve(window[name]);
-    };
-    document.head.appendChild(dom);
-  });
+import { MessageEvent, PageMessageEvent } from './event';
 
 export const clearToken = () => {
   localStorage.setItem('JWT-TOKEN', '');
@@ -216,9 +20,7 @@ export const getToken = () => {
  * <a href="url" download/>
  */
 export const downloadFile = (url, fileName, encode = true) => {
-  // if (window.InvokeUnityFileOpener) {
-  //   window.InvokeUnityFileOpener(url);
-  // } else {
+  
   const downloadLink = document.createElement('a');
   const body = document.documentElement || document.body;
   body.appendChild(downloadLink);
@@ -235,40 +37,6 @@ export const downloadFile = (url, fileName, encode = true) => {
   body.removeChild(downloadLink);
   // }
 };
-window.downloadFile = downloadFile;
-
-/**
- *
- * @param date1
- * @param date2
- * @returns {number}
- * @constructor 日期相减
- */
-export function DateMinus(date1, date2) {
-  const sdate = new Date(date1);
-  const now = new Date(date2);
-  const days = now.getTime() - sdate.getTime();
-  const day = parseInt(days / (1000 * 60 * 60 * 24), 10);
-  return day;
-}
-
-export function SendMsgToWebGL(type, data) {
-  if (window.MsgSender) {
-    window.MsgSender(type, data);
-  }
-}
-
-export function ShowUnreadNotification(notice) {
-  notification.open({
-    message: '通知',
-    description: notice.MsgBody,
-    icon: <Icon type="warning" style={{ color: '#108ee9' }} />,
-    placement: 'bottomRight',
-    onClose: () => {
-      return SetNotificationRead({ ID: notice.ID });
-    },
-  });
-}
 
 export function IsImageFile(fileName) {
   return (
@@ -321,16 +89,6 @@ export function GetFileType(url) {
   }
 }
 
-export function getAuthoriter(projectId) {
-  return request(`/user/res/${projectId}`).then(res => {
-    if (res && res.data) {
-      sessionStorage['authority'] = JSON.stringify(res.data);
-      localStorage['authority'] = JSON.stringify(res.data);
-    }
-    return res;
-  });
-}
-
 export function getUser(params) {
   let arr = [];
   if (!params) {
@@ -343,6 +101,7 @@ export function getUser(params) {
       .join(','));
   }
 }
+
 export const UnityAction = {
   event: {},
   on(type, callback) {
@@ -385,100 +144,26 @@ export const UnityAction = {
     }
   },
 };
-if (window.vuplex) {
-  window.vuplex.addEventListener('message', e => {
-    console.log('============================getMessageForUnity============================');
-    const data = JSON.parse(e.data);
-    console.log(data);
-    UnityAction.emit(data.type, data.message);
-
-    // 将消息广播给子页面
-    // let iframeDom = document.getElementsByClassName('iframe');
-    // for (let i = 0; i < iframeDom.length; i++) {
-    //   const item = iframeDom[i];
-    //   item.contentWindow.postMessage(data.type, data.message);
-    // }
-  });
-}
-
-var longClick = false,
-  timer = null,
-  touches = null;
-export const LongPress = {
-  on(ele, callback) {
-    ele.addEventListener('touchstart', function(e) {
-      longClick = false;
-      touches = {
-        clientX: e.touches[0].clientX,
-        clientY: e.touches[0].clientY,
-        screenX: e.touches[0].screenX,
-        screenY: e.touches[0].screenY,
-      };
 
-      console.log(e);
-      ele.dispatchEvent(
-        new MouseEvent('mousedown', {
-          view: window,
-          bubbles: true,
-          cancelable: true,
-        })
-      );
-      timer = setTimeout(function() {
-        longClick = true;
-        e.preventDefault();
-      }, 1000);
-    });
-    ele.addEventListener('touchmove', function(e) {
-      clearTimeout(timer);
-      longClick = false;
-    });
-    ele.addEventListener('touchend', function(e) {
-      clearTimeout(timer);
-      if (longClick) {
-        callback && callback(e, touches);
-      }
-      return false;
-    });
-  },
-  off(ele) {
-    var getEventListeners = window.getEventListeners;
-    if (getEventListeners) {
-      getEventListeners(ele).touchstart.forEach(fn => el.removeEventListener(fn));
-      getEventListeners(ele).touchmove.forEach(fn => el.removeEventListener(fn));
-      getEventListeners(ele).touchend.forEach(fn => el.removeEventListener(fn));
-    }
-  },
-};
-// export const EventBus = {
-//   event: {},
-//   on(type, callback) {
-//     if (!EventBus.event[type]) {
-//       EventBus.event[type] = [];
-//     }
-//     EventBus.event[type].push(callback);
-//   },
-//   off(type, callback) {
-//     if (callback) {
-//       let index = EventBus.event[type].indexOf(callback);
-//       if (index == -1) return;
-//       EventBus.event[type].splice(index, 1);
-//     } else {
-//       EventBus.event[type] = [];
-//     }
-//     window[type] = null;
-//   },
-//   emit(type, e) {
-//     if (!EventBus.event[type]) return;
-//     console.log('emit====== type:', type, '====== event:', e);
-//     EventBus.event[type].forEach(item => {
-//       item && item(e);
-//     });
-//   },
-// };
-
-export const EventBus = new MessageEvent();
+// if (window.vuplex) {
+//   window.vuplex.addEventListener('message', e => {
+//     console.log('============================getMessageForUnity============================');
+//     const data = JSON.parse(e.data);
+//     console.log(data);
+//     UnityAction.emit(data.type, data.message);
+
+//     // 将消息广播给子页面
+//     // let iframeDom = document.getElementsByClassName('iframe');
+//     // for (let i = 0; i < iframeDom.length; i++) {
+//     //   const item = iframeDom[i];
+//     //   item.contentWindow.postMessage(data.type, data.message);
+//     // }
+//   });
+// }
+
+// export const EventBus = new MessageEvent();
 // export const UnityAction = new UnityMessageEvent();
-export const PageAction = new PageMessageEvent();
+// export const PageAction = new PageMessageEvent();
 
 export function getGlobalData(key) {
   let data;
@@ -490,6 +175,7 @@ export function getGlobalData(key) {
 
   return key ? data[key] : data;
 }
+
 export function setGlobalData(key, value) {
   let data;
   if (!key) return;
@@ -501,6 +187,7 @@ export function setGlobalData(key, value) {
   data[key] = value;
   localStorage.GLOBAL_DATA = JSON.stringify(data);
 }
+
 // 根据token缓存数据
 export function saveData(key, data) {
   let token = GetTokenFromUrl() || getToken();

+ 1 - 8
tsconfig.json

@@ -1,10 +1,3 @@
 {
-  "extends": "./src/.umi/tsconfig.json",
-  "compilerOptions":{
-    "paths": {
-      "@/*": ["./src/*"],
-      "@@/*": ["./src/.umi/*"]
-    }
-  }
-  
+  "extends": "./src/.umi/tsconfig.json"
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов