Pārlūkot izejas kodu

feat: 初始化flow组件,移除部分路由页面代码

lanjianrong 3 gadi atpakaļ
vecāks
revīzija
d9ac4da88d
29 mainītis faili ar 442 papildinājumiem un 400 dzēšanām
  1. 6 6
      config/routes.ts
  2. 0 0
      src/components/Flow/index.ts
  3. 16 0
      src/components/Flow/src/enum/index.ts
  4. 208 0
      src/components/Flow/src/shared/transformer.ts
  5. 34 0
      src/components/Flow/src/type.ts
  6. 12 13
      src/pages/Business/Process/hooks/useScripts.tsx
  7. 5 15
      src/pages/Business/Process/index.tsx
  8. 0 279
      src/pages/Business/Process/schema.ts
  9. 0 0
      src/pages/Business/Step/components/Flow/components/Drawer/ParticipantCard.tsx
  10. 2 8
      src/pages/Project/Verification/Detail/Flow/components/Drawer/index.less
  11. 0 0
      src/pages/Business/Step/components/Flow/components/Drawer/index.tsx
  12. 5 7
      src/pages/Project/Verification/Detail/Flow/components/Edge/index.less
  13. 15 13
      src/pages/Project/Verification/Detail/Flow/components/Edge/index.tsx
  14. 2 2
      src/pages/Project/Verification/Detail/Flow/components/Graph/FloatingEdge.tsx
  15. 6 10
      src/pages/Project/Verification/Detail/Flow/components/Graph/index.tsx
  16. 14 9
      src/pages/Project/Verification/Detail/Flow/components/Node/index.tsx
  17. 2 5
      src/pages/Project/Verification/Detail/Flow/components/Toolbar/index.tsx
  18. 0 0
      src/pages/Business/Step/components/Flow/context/index.tsx
  19. 0 0
      src/pages/Business/Step/components/Flow/context/reducer.ts
  20. 6 0
      src/pages/Project/Verification/Detail/Flow/enum/index.ts
  21. 0 0
      src/pages/Business/Step/components/Flow/hooks/useLoading.tsx
  22. 11 11
      src/pages/Project/Verification/Detail/Flow/index.less
  23. 8 5
      src/pages/Project/Verification/Detail/Flow/index.tsx
  24. 56 0
      src/pages/Business/Step/components/Flow/shared/transformer.ts
  25. 5 5
      src/pages/Business/Step/components/Flow/utils.ts
  26. 2 7
      src/pages/Project/Verification/Detail/index.tsx
  27. 0 4
      src/pages/Project/Verification/index.tsx
  28. 1 1
      src/services/api/project.ts
  29. 26 0
      src/services/api/typings.d.ts

+ 6 - 6
config/routes.ts

@@ -80,13 +80,13 @@ const routes: Route[] = [
         name: BackstagePermission.VIEW_APPROVAL_ROLE,
         access: 'authRouteFilter',
         component: './Project/Approval'
-      },
-      {
-        path: '/project/verification',
-        name: BackstagePermission.VIEW_AUDIT_TEMPLATE,
-        access: 'authRouteFilter',
-        component: './Project/Verification'
       }
+      // {
+      //   path: '/project/verification',
+      //   name: BackstagePermission.VIEW_AUDIT_TEMPLATE,
+      //   access: 'authRouteFilter',
+      //   component: './Project/Verification'
+      // }
     ]
   },
   {

+ 0 - 0
src/components/Flow/index.ts


+ 16 - 0
src/components/Flow/src/enum/index.ts

@@ -0,0 +1,16 @@
+export enum NodeType {
+  /** 标准节点 */
+  COMMON = 'common',
+  START = 'start',
+  END = 'end',
+  /** 操作节点 */
+  OPERATION = 'operation',
+  /** 分支区块 */
+  BLOCK = 'block',
+  /** 条件节点 */
+  CONDITION = 'condition'
+}
+
+export enum EdgeType {
+  COMMON = 'common'
+}

+ 208 - 0
src/components/Flow/src/shared/transformer.ts

@@ -0,0 +1,208 @@
+import { createUid } from '@/utils/util'
+import { max, omit } from 'lodash'
+import { ArrowHeadType, Edge, Elements, FlowElement, Node, XYPosition } from 'react-flow-renderer'
+import { NodeType, EdgeType } from '../enum'
+import { FlowNode, TransformOptions, ComposeOptions } from '../type'
+
+/** 返回condition节点中最长的那一条的长度 */
+const calcMaxConditionLen = (condition: FlowNow): number => {
+  const stack = []
+  condition.children?.forEach(item => {
+    if (item.type === NodeType.CONDITION) {
+      stack.push(calcMaxConditionLen())
+    } else {
+      stack.push(item.children?.length || 0)
+    }
+  })
+  return max(stack)
+}
+
+/**
+ * 传入两个node组合生成新的elments
+ * @param connectedNode 上一个node节点
+ * @param composeNode 需要组合的节点
+ * @param options 额外配置参数
+ */
+const composeElement = (connectedNode: Node, composeNode: Node, options: ComposeOptions): FlowElement[] => {
+  const { space, order, offset = 0, direction } = options
+  // 组合好的node
+  const composedNode: Node = { ...omit(composeNode, ['children', 'order']) }
+  // 根据level,order计算postition
+  const position: XYPosition = {}
+  if (direction === 'x') {
+    position.x = (order + 1) * space
+    position.y = 400 + offset
+  } else {
+    position.x = 400 + offset
+    position.y = (order + 1) * space
+    // TODO: 这里要考虑不同level的x的位置
+  }
+  composedNode.position = position
+  // 组合好的edge
+  const edge: Edge = {
+    id: createUid(),
+    source: options?.reverse ? composeNode.id : connectedNode?.id,
+    target: options?.reverse ? connectedNode.id : composeNode.id,
+    type: options.type ?? EdgeType.COMMON,
+    arrowHeadType: ArrowHeadType.Arrow
+  }
+  return [composedNode, edge]
+}
+
+/** 通过传入nodes,构建edges */
+export function transformElements<T>(
+  nodes: FlowNode<T>[],
+  options: TransformOptions = { space: 200, direction: 'y' }
+): Elements<T> {
+  const firstNode = { id: 'start', type: 'start', position: { x: 400, y: 0 } }
+  const lastNode = { id: 'end', type: 'end' }
+  const elements: Elements<T> = []
+  let lastOrder = 0
+  // 遍历节点
+  function generateElements(
+    nodes: FlowNode<T>[],
+    offset?: number | null,
+    start?: FlowNode<T>,
+    end?: FlowNode<T>
+  ) {
+    nodes.forEach((item, idx) => {
+      let baseOrder = 0
+
+      if (nodes[idx - 1]?.type === NodeType.OPERATION) {
+        // 它的上一个可能是[添加条件按钮]操作节点, 则需要用最大长度的块为基座进行order累加
+        // TODO: 将来要考虑多条件节点,目前这里的逻辑是单条件
+        baseOrder = calcMaxConditionLen(nodes[idx - 1])
+        baseOrder > lastOrder && (lastOrder = baseOrder)
+      }
+      // 通用类型
+      if (item.type === NodeType.COMMON) {
+        // 1. 是否开头
+        if (idx === 0) {
+          const [element, edge] = composeElement(start ?? firstNode, item, {
+            offset,
+            order: baseOrder + (item.order || idx),
+            space: options.space,
+            direction: options.direction
+          })
+          elements.push(element, edge)
+        } else if (idx === nodes.length - 1) {
+          // 2. 是否结尾
+          if (!end) {
+            // TODO: 是否考虑多条件节点下的归属问题
+            // 真的结尾了
+            lastOrder += idx
+          }
+          const [element, edge] = composeElement(end ?? lastNode, item, {
+            reverse: true,
+            offset,
+            order: baseOrder + (item.order || idx),
+            space: options.space,
+            direction: options.direction
+          })
+          elements.push(element, edge)
+        } else {
+          // 3. 中间
+          const [element, edge] = composeElement(nodes[nodes.length - 1 - idx], item, {
+            offset,
+            order: baseOrder + (item.order || idx),
+            space: options.space,
+            direction: options.direction
+          })
+          elements.push(element, edge)
+        }
+      } else if (item.type === NodeType.OPERATION) {
+        const [element, edge] = composeElement(nodes[idx - 1], item, {
+          order: idx,
+          space: options.space,
+          direction: options.direction
+        })
+
+        elements.push(element, edge)
+        generateElements(
+          item?.children.map(item => ({ ...item, order: idx })) || [],
+          null,
+          item,
+          idx === nodes.length - 1 ? lastNode : nodes[idx + 1]
+        )
+      } else if (item.type === NodeType.BLOCK) {
+        // 区域节点, 该节点只是占位,直接遍历子节点
+        const len = nodes.length // 一共有几个条件分支
+        const intermediation = len / 2
+        // 一次算出offset,因为该区域节点的offset都是一致的
+        let offset
+        if (len % 2 === 0) {
+          if (idx < intermediation) {
+            // 算出的当前offset,传入直接算出偏移的x/y
+            offset = (idx - intermediation) * 50 + 25
+          } else {
+            offset = (idx - intermediation) * 50 - 25
+          }
+        } else {
+          offset = (idx - intermediation) * 50
+        }
+
+        return generateElements(
+          item.children.map((child, idx) => ({ ...child, order: item.order + 1 + idx })),
+          offset,
+          start,
+          end
+        )
+      }
+    })
+  }
+  generateElements(nodes)
+
+  return [firstNode, ...elements, { ...lastNode, position: { x: 400, y: (lastOrder + 1) * 200 } }]
+}
+
+// 点击添加条件
+// const node = [
+//   {
+//     id: 'y-1',
+//     type: 'common'
+//   },
+//   {
+//     id: 'xxx',
+//     name: '添加条件', // 用type可替代
+//     type: 'operation',
+//     children: [
+//       {
+//         id: 'xxx-1',
+//         type: 'block',
+//         children: [
+//           {
+//             id: 'xxx-1-1',
+//             type: 'condition'
+//           },
+//           // 正常的节点数据
+//           {
+//             id: 'xxx-1-2',
+//             type: 'common'
+//           }
+//         ]
+//       },
+//       {
+//         id: 'xxx-2',
+//         type: 'block',
+//         children: [
+//           {
+//             id: 'xxx-2-1',
+//             type: 'condition'
+//           },
+//           {
+//             id: 'xxx-2-2',
+//             type: 'common'
+//           },
+//           {
+//             id: 'xxx-2-3',
+//             type: 'common'
+//           }
+//         ]
+//       }
+//     ]
+//   },
+//   {
+//     id: 'y-2',
+//     type: 'common'
+//   }
+// ]

+ 34 - 0
src/components/Flow/src/type.ts

@@ -0,0 +1,34 @@
+import { Node } from 'react-flow-renderer'
+import { EdgeType, NodeType } from './enum'
+
+export type FlowNode<T> = {
+  children: Node<T>[]
+} & Node<T>
+
+export type TransformOptions = {
+  /** @name 主轴的间距 */
+  space?: number
+  /** @name 全局的流程走向,局部优先级更大 */
+  direction?: 'x' | 'y'
+}
+
+export type ComposeOptions = {
+  type?: EdgeType
+  /** @name 反转source、target的指向 */
+  reverse?: boolean
+  /** @name 条件分支副轴的偏移量 */
+  offset?: number
+  /** @name 流程顺序 */
+  order: number
+  /** @name 条件分支的父亲节点 */
+  parentId?: string
+  /** @name 条件分支的终点 */
+  end: boolean
+} & TransformOptions
+
+export type FlowTreeNode = {
+  id?: string
+  type?: NodeType
+  data?: Record<string, any>
+  children?: FlowTreeNode[]
+}

+ 12 - 13
src/pages/Business/Process/hooks/useScripts.tsx

@@ -6,7 +6,7 @@ import { addExecutor, delExecutor, queryExecutorList, updateExecutor } from '@/s
 import { ApprovalType, BusinessType, ExecutorSetType } from '@/enums/gc'
 import { QuestionCircleFilled } from '@ant-design/icons'
 import { Form, message, Tooltip } from 'antd'
-import ProForm, { ProFormRadio, ProFormText } from '@ant-design/pro-form'
+import ProForm, { ProFormDependency, ProFormRadio, ProFormSelect, ProFormText } from '@ant-design/pro-form'
 
 import type { ModalAction } from '@/components/Modal'
 
@@ -39,7 +39,6 @@ type iState = {
   items: API.ExecutorItem[]
   total: number
 }
-const approvalTypeOptions = [{ label: '指定用户', value: ApprovalType.TARGET }]
 
 export default function useScripts(modal: ModalAction, business: BusinessType) {
   const [state, seState] = useState<iState>({
@@ -53,8 +52,6 @@ export default function useScripts(modal: ModalAction, business: BusinessType) {
   })
 
   const addOrEdit = (mode: 'add' | 'edit', initialValues?: API.ExecutorItem) => {
-    console.log(initialValues)
-
     const text = mode === 'add' ? '新增执行者' : '编辑执行者'
     modal.open({
       title: text,
@@ -80,15 +77,17 @@ export default function useScripts(modal: ModalAction, business: BusinessType) {
             options={setTypeOptions}
             rules={[{ required: true, message: '请选择' }]}
           />
-          <ProFormRadio.Group
-            label="审批模式"
-            name="approvalType"
-            options={approvalTypeOptions}
-            rules={[{ required: true, message: '请选择' }]}
-          />
-          <Form.Item label="成员" name="members" rules={[{ required: true, message: '请选择' }]}>
-            <MemberItem />
-          </Form.Item>
+          <ProFormDependency name={['setType']}>
+            {({ name }) => {
+              if (name === ExecutorSetType.PRESET_MEMBER)
+                return (
+                  <Form.Item label="成员" name="members" rules={[{ required: true, message: '请选择' }]}>
+                    <MemberItem />
+                  </Form.Item>
+                )
+              return <ProFormSelect name="" label="范围选择" />
+            }}
+          </ProFormDependency>
         </ProForm>
       ),
       onOk: async (values: API.ExecutorItem) => {

+ 5 - 15
src/pages/Business/Process/index.tsx

@@ -2,7 +2,7 @@ import LeftMenu from './components/LeftMenu'
 import useModal from '@/components/Modal'
 import { useState } from 'react'
 import { PageContainer } from '@ant-design/pro-layout'
-import { BusinessType, ApprovalType } from '@/enums/gc'
+import { BusinessType, ExecutorSetType } from '@/enums/gc'
 import useScripts from './hooks/useScripts'
 import { Button } from 'antd'
 import ProTable, { ProColumnType } from '@ant-design/pro-table'
@@ -32,21 +32,15 @@ const Process = () => {
       dataIndex: 'setType',
       title: '配置方式',
       align: 'center',
-      width: 100
-    },
-    {
-      dataIndex: 'approvalType',
-      title: '审批模式',
       width: 100,
-      align: 'center',
       valueEnum: {
-        [ApprovalType.TARGET]: { text: '指定用户' }
+        [ExecutorSetType.PRESET_MEMBER]: { text: '预置成员' },
+        [ExecutorSetType.STEP_SETTING]: { text: '步骤配置' }
       }
     },
     {
       dataIndex: 'members',
       title: '成员',
-      width: 240,
       onHeaderCell: () => ({ style: { textAlign: 'center' } }),
       ellipsis: true,
       renderText: (_, record) => (
@@ -58,14 +52,10 @@ const Process = () => {
       )
     },
     {
-      dataIndex: 'step',
-      title: '步骤',
-      onHeaderCell: () => ({ style: { textAlign: 'center' } })
-    },
-    {
       dataIndex: 'opreate',
       title: '操作',
       align: 'center',
+      width: 120,
       renderText: (_, record) => (
         <div className="divide-x divide-bg-gray-400 flex flex-row justify-center">
           <span
@@ -86,7 +76,7 @@ const Process = () => {
     <PageContainer title={false}>
       <div className="h-full w-full flex flex-row">
         <LeftMenu
-          title="流程用户设置"
+          title="业务列表"
           options={menuOptions}
           value={state.activeKey}
           onChange={key => setState({ ...state, activeKey: key })}

+ 0 - 279
src/pages/Business/Process/schema.ts

@@ -1,279 +0,0 @@
-export const schema = {
-  type: 'object',
-  properties: {
-    receiver: {
-      type: 'object',
-      'x-validator': [],
-      name: 'receiver',
-      'x-designable-id': 'uzrnxyxj8l2',
-      'x-index': 0,
-      properties: {
-        j6t9tp83i49: {
-          type: 'void',
-          'x-component': 'FormGrid',
-          'x-validator': [],
-          'x-component-props': {
-            minColumns: 2
-          },
-          'x-designable-id': 'j6t9tp83i49',
-          'x-index': 0,
-          properties: {
-            receiverInstitutionID: {
-              title: '收件员',
-              'x-decorator': 'FormItem',
-              'x-component': 'Select',
-              'x-validator': [],
-              'x-component-props': {
-                // style: { width: '150px' }
-              },
-              'x-decorator-props': {},
-              name: 'receiverInstitutionID',
-              required: true,
-              'x-designable-id': 'ggfc3yhph1q',
-              'x-index': 0
-            },
-            receiverAccountIDs: {
-              title: '',
-              'x-decorator': 'FormItem',
-              'x-component': 'TreeSelect',
-              'x-validator': [],
-              'x-component-props': {
-                treeCheckable: true,
-                treeDefaultExpandAll: true,
-                showSearch: true,
-                showArrow: true,
-                labelInValue: false,
-                autoFocus: false,
-                allowClear: true,
-                maxTagCount: 2
-              },
-              'x-decorator-props': {},
-              required: true,
-              name: 'receiverAccountIDs',
-              'x-designable-id': 'h3lybqrqozk',
-              'x-index': 1
-            }
-          }
-        }
-      }
-    },
-    fgLeader: {
-      type: 'object',
-      'x-validator': [],
-      name: 'fgLeader',
-      'x-designable-id': 'h2winr4c2sl',
-      'x-index': 1,
-      properties: {
-        ctp8nw51qvl: {
-          type: 'void',
-          'x-component': 'FormGrid',
-          'x-validator': [],
-          'x-component-props': {
-            minColumns: 2
-          },
-          'x-designable-id': 'ctp8nw51qvl',
-          'x-index': 0,
-          properties: {
-            fgLeaderInstitutionID: {
-              title: '分管领导',
-              'x-decorator': 'FormItem',
-              'x-component': 'Select',
-              'x-validator': [],
-              'x-component-props': {
-                // style: { width: '150px' }
-              },
-              'x-decorator-props': {},
-              name: 'fgLeaderInstitutionID',
-              required: true,
-              'x-designable-id': 'lv362lk8zoh',
-              'x-index': 0
-            },
-            fgLeaderAccountIDs: {
-              title: '',
-              'x-decorator': 'FormItem',
-              'x-component': 'TreeSelect',
-              'x-validator': [],
-              'x-component-props': {
-                allowClear: true,
-                showArrow: true,
-                showSearch: true,
-                treeDefaultExpandAll: true,
-                treeCheckable: true,
-                maxTagCount: 2
-              },
-              'x-decorator-props': {},
-              name: 'fgLeaderAccountIDs',
-              required: true,
-              'x-designable-id': 'kosbw33j1wy',
-              'x-index': 1
-            }
-          }
-        }
-      }
-    },
-    zxLeader: {
-      type: 'object',
-      'x-validator': [],
-      name: 'zxLeader',
-      'x-designable-id': 'ls0xboaj8sc',
-      'x-index': 2,
-      properties: {
-        v0qrosfsw3p: {
-          type: 'void',
-          'x-component': 'FormGrid',
-          'x-validator': [],
-          'x-component-props': {
-            minColumns: 2
-          },
-          'x-designable-id': 'v0qrosfsw3p',
-          'x-index': 0,
-          properties: {
-            zxLeaderInstitutionID: {
-              title: '中心领导',
-              'x-decorator': 'FormItem',
-              'x-component': 'Select',
-              'x-validator': [],
-              'x-component-props': {
-                // style: { width: '150px' }
-              },
-              'x-decorator-props': {},
-              name: 'zxLeaderInstitutionID',
-              'x-designable-id': 'p31hkjixl16',
-              'x-index': 0,
-              required: true
-            },
-            zxLeaderAccountIDs: {
-              title: '',
-              'x-decorator': 'FormItem',
-              'x-component': 'TreeSelect',
-              'x-validator': [],
-              'x-component-props': {
-                allowClear: true,
-                labelInValue: false,
-                showArrow: true,
-                showSearch: true,
-                treeCheckable: true,
-                treeDefaultExpandAll: true,
-                maxTagCount: 2
-              },
-              'x-decorator-props': {},
-              name: 'zxLeaderAccounts',
-              required: true,
-              'x-designable-id': 'vbu2agjbkia',
-              'x-index': 1
-            }
-          }
-        }
-      }
-    },
-    post: {
-      type: 'object',
-      'x-validator': [],
-      name: 'post',
-      'x-designable-id': '7jrdrzcwl4z',
-      'x-index': 3,
-      properties: {
-        nxombc1rrjd: {
-          type: 'void',
-          'x-component': 'FormGrid',
-          'x-validator': [],
-          'x-component-props': {
-            minColumns: 2
-          },
-          'x-designable-id': 'nxombc1rrjd',
-          'x-index': 0,
-          properties: {
-            postInstitutionID: {
-              title: '发文',
-              'x-decorator': 'FormItem',
-              'x-component': 'Select',
-              'x-validator': [],
-              'x-component-props': {
-                // style: { width: '150px' }
-              },
-              'x-decorator-props': {},
-              name: 'postInstitutionID',
-              'x-designable-id': '1sohvz4sbw5',
-              'x-index': 0,
-              required: true
-            },
-            postAccountIDs: {
-              title: '',
-              'x-decorator': 'FormItem',
-              'x-component': 'TreeSelect',
-              'x-validator': [],
-              'x-component-props': {
-                labelInValue: false,
-                showArrow: true,
-                showSearch: true,
-                treeCheckable: true,
-                treeDefaultExpandAll: true,
-                maxTagCount: 2
-              },
-              'x-decorator-props': {},
-              name: 'postAccountIDs',
-              required: true,
-              'x-designable-id': 'lw5p0zkp266',
-              'x-index': 1
-            }
-          }
-        }
-      }
-    },
-    archive: {
-      type: 'object',
-      'x-validator': [],
-      name: 'archive',
-      'x-designable-id': '8ou2kur3tku',
-      'x-index': 4,
-      properties: {
-        archive: {
-          type: 'void',
-          'x-component': 'FormGrid',
-          'x-validator': [],
-          'x-component-props': {
-            minColumns: 2
-          },
-          name: 'archive',
-          'x-designable-id': 'fle7qp5yshx',
-          'x-index': 0,
-          properties: {
-            archiveInstitutionID: {
-              title: '归档',
-              'x-decorator': 'FormItem',
-              'x-component': 'Select',
-              'x-validator': [],
-              'x-component-props': {
-                // style: { width: '150px' }
-              },
-              'x-decorator-props': {},
-              name: 'archiveInstitutionID',
-              'x-designable-id': 'gfbvoofphoo',
-              'x-index': 0,
-              required: true
-            },
-            archiveAccountIDs: {
-              title: '',
-              'x-decorator': 'FormItem',
-              'x-component': 'TreeSelect',
-              'x-validator': [],
-              'x-component-props': {
-                showArrow: true,
-                showSearch: true,
-                treeCheckable: true,
-                treeDefaultExpandAll: true,
-                maxTagCount: 2
-              },
-              'x-decorator-props': {},
-              required: true,
-              name: 'archiveAccountIDs',
-              'x-designable-id': 'ynu0ga6cip0',
-              'x-index': 1
-            }
-          }
-        }
-      }
-    }
-  },
-  'x-designable-id': 'khexddrjzg0'
-}

src/pages/Project/Verification/Detail/Flow/components/Drawer/ParticipantCard.tsx → src/pages/Business/Step/components/Flow/components/Drawer/ParticipantCard.tsx


+ 2 - 8
src/pages/Project/Verification/Detail/Flow/components/Drawer/index.less

@@ -6,16 +6,10 @@
     height: 24px;
     padding: 0;
   }
-  :global(.ant-collapse-ghost
-      > .ant-collapse-item
-      > .ant-collapse-content
-      > .ant-collapse-content-box) {
+  :global(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
     padding: 4px 0;
   }
-  :global(.ant-collapse-icon-position-right
-      > .ant-collapse-item
-      > .ant-collapse-header
-      .ant-collapse-arrow) {
+  :global(.ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header .ant-collapse-arrow) {
     right: 0;
   }
   :global(.ant-collapse > .ant-collapse-item > .ant-collapse-header .ant-collapse-arrow) {

src/pages/Project/Verification/Detail/Flow/components/Drawer/index.tsx → src/pages/Business/Step/components/Flow/components/Drawer/index.tsx


+ 5 - 7
src/pages/Project/Verification/Detail/Flow/components/Edge/index.less

@@ -8,13 +8,11 @@
   justify-content: center;
   padding: 10px;
   :global(.ant-btn) {
-    // color: #e4e4e7;
-    // box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
     transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-    &:hover {
-      // color: #40a9ff;
-      // box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
-      transform: scale(1.3);
-    }
+    // &:visited {
+    //   // color: #40a9ff;
+    //   // box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+    //   transform: scale(1.3);
+    // }
   }
 }

+ 15 - 13
src/pages/Project/Verification/Detail/Flow/components/Edge/index.tsx

@@ -1,5 +1,5 @@
 import { buildUUID } from '@/utils/uuid'
-import { PlusOutlined, SolutionOutlined } from '@ant-design/icons'
+import { BranchesOutlined, PlusOutlined, SolutionOutlined } from '@ant-design/icons'
 import { Button, Popover } from 'antd'
 import React, { useMemo, useState, useContext } from 'react'
 import type { EdgeProps } from 'react-flow-renderer'
@@ -11,14 +11,9 @@ import styles from './index.less'
 import 'antd/lib/button/style/css'
 import consts from '@/utils/consts'
 import useLoading from '../../hooks/useLoading'
-import { ConfigureType } from '../../enum'
+import { ConfigureType, SectorType } from '../../enum'
 const foreignObjectSize = 50
 
-export enum SectorType {
-  APPROVAL = 'approval',
-  CONDITION = 'condition'
-}
-
 export function CommonEdge(props: EdgeProps) {
   const {
     id,
@@ -131,7 +126,14 @@ export function CommonEdge(props: EdgeProps) {
           height={foreignObjectSize}
           x={edgeCenterX - foreignObjectSize / 2}
           y={edgeCenterY - foreignObjectSize / 2}>
-          <div className={styles.addIcon}>
+          <div
+            className={styles.addIcon}
+            onBlur={e => {
+              if (!e.currentTarget.contains(e.relatedTarget)) {
+                // Not triggered when swapping focus between children
+                togglePopver(false)
+              }
+            }}>
             <Popover
               content={
                 <ul className="m-0 p-0">
@@ -140,11 +142,11 @@ export function CommonEdge(props: EdgeProps) {
                       审批人
                     </Button>
                   </li>
-                  {/* <li className="mt-2 condition">
-        <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
-          条件分支
-        </Button>
-      </li> */}
+                  <li className="mt-2 condition">
+                    <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
+                      条件分支
+                    </Button>
+                  </li>
                 </ul>
               }
               trigger="click"

+ 2 - 2
src/pages/Project/Verification/Detail/Flow/components/Graph/FloatingEdge.tsx

@@ -27,10 +27,10 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, marker
   })
 
   return (
-    <g className="react-flow__connection">
+    <g className="react-flow-connection">
       <path
         id={id}
-        className="react-flow__edge-path"
+        className="react-flow-edge-path"
         d={d}
         markerEnd={markerEnd}
         style={style as CSSProperties}

+ 6 - 10
src/pages/Project/Verification/Detail/Flow/components/Graph/index.tsx

@@ -1,9 +1,9 @@
-import React, { useContext, useEffect, useMemo } from 'react'
+import React, { useContext, useMemo } from 'react'
 import ReactFlow, { Background, MiniMap, Controls } from 'react-flow-renderer'
-import { Actions, FlowContext, initialElements } from '../../context'
+import { Actions, FlowContext } from '../../context'
 
 import type { Elements, NodeTypesType, EdgeTypesType } from 'react-flow-renderer'
-import { CommonNode, InputNode, OutputNode } from '../Node'
+import { CommonNode, ConditionHanderNode, InputNode, OutputNode } from '../Node'
 import { CommonEdge } from '../Edge'
 
 const edgeTypes: EdgeTypesType = {
@@ -22,18 +22,14 @@ type FlowGrophProps = {
   readPretty: boolean
   flowProcessData: API.ApprovalProcessItem[]
 }
-const FlowGroph: React.FC<FlowGrophProps> = ({
-  dataID,
-  flowProcess,
-  flowProcessData,
-  readPretty
-}) => {
+const FlowGroph: React.FC<FlowGrophProps> = ({ dataID, flowProcess, flowProcessData, readPretty }) => {
   const { flowState, dispatch } = useContext(FlowContext)
   const nodeTypes: NodeTypesType = useMemo(
     () => ({
       start: InputNode,
       end: OutputNode,
-      common: CommonNode
+      common: CommonNode,
+      conditionHander: ConditionHanderNode
     }),
     []
   )

+ 14 - 9
src/pages/Project/Verification/Detail/Flow/components/Node/index.tsx

@@ -5,7 +5,7 @@ import { CloseOutlined, RightOutlined } from '@ant-design/icons'
 import { removeApprovalNode } from '@/services/api/project'
 import consts from '@/utils/consts'
 import { generateElements, genreateElementEnum } from '../../utils'
-import { message } from 'antd'
+import { Button } from 'antd'
 import useLoading from '../../hooks/useLoading'
 import classNames from 'classnames'
 export const InputNode = () => {
@@ -36,6 +36,17 @@ export const OutputNode = () => {
   )
 }
 
+export const ConditionHanderNode = () => {
+  return (
+    <>
+      <Handle type="target" position="top" />
+      <Button type="primary">添加条件</Button>
+      <Handle type="source" position="left" />
+      <Handle type="target" position="right" />
+    </>
+  )
+}
+
 export const CommonNode = ({ id, data }) => {
   const { flowState, dispatch } = useContext(FlowContext)
   const { flowInstance, flowData, readPretty } = flowState
@@ -94,9 +105,7 @@ export const CommonNode = ({ id, data }) => {
         <div className="relative text-light-500  p-1 flex justify-between items-center">
           <div className="leading-5 h-5 text-12px">审批人</div>
           {!readPretty ? (
-            <div
-              className="remove_node w-24px h-24px bg-hex-ff4d4f align-middle"
-              onClick={removeNode}>
+            <div className="remove_node w-24px h-24px bg-hex-ff4d4f align-middle" onClick={removeNode}>
               <CloseOutlined size={12} />
             </div>
           ) : null}
@@ -108,11 +117,7 @@ export const CommonNode = ({ id, data }) => {
           )}
           onClick={openDrawer}>
           <div>
-            {auditor ? (
-              auditor.accounts?.[0]?.name
-            ) : (
-              <span className="text-hex-999999">发起人自选</span>
-            )}
+            {auditor ? auditor.accounts?.[0]?.name : <span className="text-hex-999999">发起人自选</span>}
           </div>
           <div>
             <RightOutlined />

+ 2 - 5
src/pages/Project/Verification/Detail/Flow/components/Toolbar/index.tsx

@@ -1,16 +1,13 @@
 import { CloudUploadOutlined } from '@ant-design/icons'
 import { useContext } from 'react'
 import { Button, message, Modal } from 'antd'
-import { FlowContext, InitialState } from '../../context'
+import { FlowContext } from '../../context'
 import { addApprovalFlow } from '@/services/api/project'
 import consts from '@/utils/consts'
 import { isNode } from 'react-flow-renderer'
 import type { Elements } from 'react-flow-renderer'
 
-export default function ToolBar(props: {
-  dataId: string
-  closeAnimateContent: (visible: boolean) => void
-}) {
+export default function ToolBar(props: { dataId: string; closeAnimateContent: (visible: boolean) => void }) {
   const { dataId, closeAnimateContent } = props
   const { flowState } = useContext(FlowContext)
 

src/pages/Project/Verification/Detail/Flow/context/index.tsx → src/pages/Business/Step/components/Flow/context/index.tsx


src/pages/Project/Verification/Detail/Flow/context/reducer.ts → src/pages/Business/Step/components/Flow/context/reducer.ts


+ 6 - 0
src/pages/Project/Verification/Detail/Flow/enum/index.ts

@@ -9,6 +9,12 @@ export enum Actions {
   CLOSE_MODAL = 'close_modal'
 }
 
+/** 环节类型 */
+export enum SectorType {
+  APPROVAL = 'approval',
+  CONDITION = 'condition'
+}
+
 /** 审批方式 */
 export enum ApprovalWay {
   /** @name 指定用户 */

src/pages/Project/Verification/Detail/Flow/hooks/useLoading.tsx → src/pages/Business/Step/components/Flow/hooks/useLoading.tsx


+ 11 - 11
src/pages/Project/Verification/Detail/Flow/index.less

@@ -1,16 +1,16 @@
 .flow-container {
-  & .react-flow__node {
+  & .react-flow-node {
     width: 200px;
     max-height: 86px;
-    border-radius: 8px 8px 8px 8px;
+    border-radius: 8px;
     opacity: 1;
   }
-  & .react-flow__handle {
+  & .react-flow-handle {
     opacity: 0;
   }
-  .node_content {
+  .node-content {
     cursor: default;
-    .remove_node {
+    .remove-node {
       transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
       position: absolute;
       top: -13px;
@@ -19,29 +19,29 @@
       align-items: center;
       justify-content: center;
       border: 1px solid #ff4d4f;
-      border-radius: 99px 99px 99px 99px;
-      box-shadow: 0 2px 0 1px rgba(0, 0, 0, 0.0430000014603138);
+      border-radius: 99px;
+      box-shadow: 0 2px 0 1px rgba(0 0 0 / 4.3%);
       cursor: pointer;
       opacity: 0;
     }
-    .operate_node {
+    .operate-node {
       cursor: pointer;
     }
     &:hover {
       border-color: blue;
     }
-    &:hover .remove_node {
+    &:hover .remove-node {
       opacity: 1;
     }
   }
-  & .react-flow__controls {
+  & .react-flow-controls {
     bottom: 90%;
     left: 90%;
     display: flex;
     justify-content: end;
     // margin: 12px;
     box-shadow: none;
-    & .react-flow__controls-zoomout {
+    & .react-flow-controls-zoomout {
       margin-left: 8px;
     }
   }

+ 8 - 5
src/pages/Project/Verification/Detail/Flow/index.tsx

@@ -18,12 +18,15 @@ const ApprovalFlow: React.FC<ApprovalFlowProps> = ({ readPretty, dataID }) => {
     flowProcess: null,
     flowProcessData: null
   })
-  const { run: tryGetDetail } = useRequest(queryApprovalDetail, {
-    manual: true,
-    onSuccess: result => {
-      setState({ ...state, flowProcess: result.flowProcess, flowProcessData: result.process })
+  const { run: tryGetDetail } = useRequest(
+    () => queryApprovalDetail({ subjectID: 'c3336fe7-2bf4-4301-9846-b688d4dd73e8', businessType: 'ys' }),
+    {
+      manual: true,
+      onSuccess: result => {
+        setState({ ...state, flowProcess: result.flowProcess, flowProcessData: result.process })
+      }
     }
-  })
+  )
   useEffect(() => {
     if (dataID) tryGetDetail({ ID: dataID })
   }, [dataID])

+ 56 - 0
src/pages/Business/Step/components/Flow/shared/transformer.ts

@@ -0,0 +1,56 @@
+import { NodeType } from '@/components/Flow/src/enum'
+import { FlowTreeNode } from '@/components/Flow/src/type'
+import { SectorType } from '../enum'
+
+/** 将业务审批数据转化成流程节点数据 */
+export const transformToFlowNodes = (data: API.ApprovalProcess[]): FlowTreeNode[] => {
+  const flowNodes: FlowTreeNode[] = []
+  data.forEach(item => {
+    const node: FlowTreeNode = {}
+    if (item.sectorType === 'approval') {
+      Object.assign(node, {
+        id: item.ID,
+        type: NodeType.COMMON,
+        data: { participantInfo: item.participantInfo, name: item.name }
+      })
+    } else {
+      const { conditionInfo } = item
+      if (!conditionInfo?.[0]?.ID) {
+        console.error('node data is not compliant, please check')
+      }
+      node.id = item.ID
+      node.type = NodeType.OPERATION
+      // TODO: 这里未来要考虑多条件节点的逻辑
+      node.children = [
+        { id: conditionInfo?.[0]?.ID, type: NodeType.BLOCK, data: conditionInfo?.[0]?.specificCondition },
+        ...transformToFlowNodes(conditionInfo?.[0]?.process)
+      ]
+    }
+    flowNodes.push(node)
+  })
+  return flowNodes
+}
+
+/** 将流程节点数据转化成业务审批数据 */
+export const transformToApprovalProcess = (flowData: FlowTreeNode[]): API.ApprovalProcess[] => {
+  const process: API.ApprovalProcess[] = []
+  flowData
+    .filter(item => ![NodeType.START, NodeType.END].includes(item.type))
+    .forEach(item => {
+      const processItem: API.ApprovalProcess = { ID: item.id }
+      if (item.type === NodeType.COMMON) {
+        processItem.sectorType = SectorType.APPROVAL
+        processItem.participantInfo = item.data?.participantInfo || {}
+      } else if (item.type === NodeType.OPERATION) {
+        processItem.sectorType = SectorType.CONDITION
+        processItem.conditionInfo = transformToApprovalProcess(item.children)
+      } else {
+        // block块第一个是条件,后续是正常的节点
+        const [firstNode, ...resetNodes] = item.children || []
+        processItem.specificCondition = firstNode.data?.condition || []
+        processItem.process = transformToApprovalProcess(resetNodes)
+      }
+      process.push(processItem)
+    })
+  return process
+}

src/pages/Project/Verification/Detail/Flow/utils.ts → src/pages/Business/Step/components/Flow/utils.ts


+ 2 - 7
src/pages/Project/Verification/Detail/index.tsx

@@ -1,4 +1,4 @@
-import ApprovalFlow from '@/pages/Project/Verification/Detail/Flow'
+import ApprovalFlow from '@/pages/Business/Step/components/Flow'
 import type { FC } from 'react'
 type ApprovalDetailProps = {
   dataID: string
@@ -6,12 +6,7 @@ type ApprovalDetailProps = {
   readPretty?: boolean
   onVisibleChange: (visible: boolean) => void
 }
-const ApprovalDetail: FC<ApprovalDetailProps> = ({
-  dataID,
-  name,
-  readPretty = false,
-  onVisibleChange
-}) => {
+const ApprovalDetail: FC<ApprovalDetailProps> = ({ dataID, name, readPretty = false, onVisibleChange }) => {
   return (
     <div className="w-full h-full">
       <ApprovalFlow

+ 0 - 4
src/pages/Project/Verification/index.tsx

@@ -13,10 +13,6 @@ import { ModalForm, ProFormText } from '@ant-design/pro-form'
 import type { ProFormInstance } from '@ant-design/pro-form'
 import classNames from 'classnames'
 
-// export enum PublishType {
-//   FAIL = '0',
-//   SUCCESS = '1'
-// }
 export enum ApprovalModalType {
   ADD,
   UPDATE

+ 1 - 1
src/services/api/project.ts

@@ -1,4 +1,4 @@
-import type { SectorType } from '@/pages/Project/Verification/Detail/Flow/components/Edge'
+import type { SectorType } from '@/pages/Business/Step/components/Flow/components/Edge'
 import { request } from '@umijs/max'
 
 /** 获取项目列表 POST /api/project/list */

+ 26 - 0
src/services/api/typings.d.ts

@@ -445,4 +445,30 @@ declare namespace API {
     roleIDs: string
     enable: boolean
   }
+
+  type Executor = {
+    ID?: string
+    name?: string
+    executorType?: 'preset' | 'dynamic'
+    permission?: string[]
+    configure?: string[]
+  }
+  type ParticipantInfo = {
+    approvalWay?: string
+    executor?: Executor
+  }
+  type ConditionInfo = {
+    ID?: string
+    /** 条件内容 */
+    specificCondition?: { condition?: [string, string, string]; value?: string }[]
+    process?: ApprovalProcess[]
+  }
+  /** 业务审批 */
+  type ApprovalProcess = {
+    ID?: string
+    name?: string
+    sectorType?: 'approval' | 'condition'
+    participantInfo?: ParticipantInfo
+    conditionInfo?: ConditionInfo[]
+  }
 }