lanjianrong 3 лет назад
Родитель
Сommit
f3af2cad5d
34 измененных файлов с 2602 добавлено и 1053 удалено
  1. 3 3
      package.json
  2. 1313 335
      pnpm-lock.yaml
  3. 3 3
      src/components/Drawer/index.tsx
  4. 18 0
      src/components/Flow/src/edges/BaseEdge/index.less
  5. 119 0
      src/components/Flow/src/edges/BaseEdge/index.tsx
  6. 4 2
      src/components/Flow/src/enum/index.ts
  7. 218 151
      src/components/Flow/src/shared/transformer.ts
  8. 0 0
      src/components/Flow/src/types/general.ts
  9. 0 0
      src/components/Flow/src/types/index.ts
  10. 28 0
      src/components/Flow/src/types/node.ts
  11. 0 0
      src/components/Flow/src/types/utils.ts
  12. 0 25
      src/components/HeaderSearch/index.less
  13. 0 101
      src/components/HeaderSearch/index.tsx
  14. 89 0
      src/pages/Business/Step/components/Flow/components/Edge/BaseEdge.tsx
  15. 69 0
      src/pages/Business/Step/components/Flow/components/Edge/EdgeText.tsx
  16. 66 0
      src/pages/Business/Step/components/Flow/components/Edge/EdgeWithButton.tsx
  17. 222 0
      src/pages/Business/Step/components/Flow/components/Edge/SmoothStepEdge.tsx
  18. 8 1
      src/pages/Business/Step/components/Flow/components/Edge/index.less
  19. 1 0
      src/pages/Business/Step/components/Flow/components/Edge/index.ts
  20. 0 170
      src/pages/Business/Step/components/Flow/components/Edge/index.tsx
  21. 55 0
      src/pages/Business/Step/components/Flow/components/Edge/utils.ts
  22. 43 48
      src/pages/Business/Step/components/Flow/components/Graph/index.tsx
  23. 53 0
      src/pages/Business/Step/components/Flow/components/Node/BaseNode.tsx
  24. 58 0
      src/pages/Business/Step/components/Flow/components/Node/ConditionButton.tsx
  25. 14 0
      src/pages/Business/Step/components/Flow/components/Node/ConditionHanderNode.tsx
  26. 51 0
      src/pages/Business/Step/components/Flow/components/Node/ConditionNode.tsx
  27. 29 0
      src/pages/Business/Step/components/Flow/components/Node/DefaultNodes.tsx
  28. 5 0
      src/pages/Business/Step/components/Flow/components/Node/index.ts
  29. 0 130
      src/pages/Business/Step/components/Flow/components/Node/index.tsx
  30. 19 37
      src/pages/Business/Step/components/Flow/context/index.tsx
  31. 9 8
      src/pages/Business/Step/components/Flow/index.less
  32. 5 37
      src/pages/Business/Step/components/Flow/index.tsx
  33. 99 1
      src/pages/Business/Step/index.tsx
  34. 1 1
      src/services/api/project.ts

+ 3 - 3
package.json

@@ -43,11 +43,11 @@
     "@formily/reactive": "^2.1.10",
     "@formily/reactive-react": "^2.1.10",
     "@formily/shared": "^2.1.10",
-    "@umijs/max": "4.0.12",
-    "@umijs/plugins": "4.0.12",
+    "@umijs/max": "4.0.19",
+    "@umijs/plugins": "4.0.19",
     "@umijs/route-utils": "^2.2.0",
     "ahooks": "^3.0.0",
-    "antd": "4.20.7",
+    "antd": "4.23.0",
     "array-move": "^4.0.0",
     "classnames": "^2.2.6",
     "dayjs": "^1.11.3",

Разница между файлами не показана из-за своего большого размера
+ 1313 - 335
pnpm-lock.yaml


+ 3 - 3
src/components/Drawer/index.tsx

@@ -22,14 +22,14 @@ const DrawerHoc = memo(
 
     const [chidlren, setDrawerChildren] = useState<React.ReactElement>(null)
     const [drawerProps, setDrawerProps] = useState<DrawerProps>({
-      visible: false
+      open: false
     })
 
     // 关闭当前Modal
     const onClose = useCallback(() => {
       setDrawerProps(source => ({
         ...source,
-        visible: false
+        open: false
       }))
     }, [])
 
@@ -37,7 +37,7 @@ const DrawerHoc = memo(
     const onOpen = useCallback(() => {
       setDrawerProps(source => ({
         ...source,
-        visible: true
+        open: true
       }))
     }, [])
 

+ 18 - 0
src/components/Flow/src/edges/BaseEdge/index.less

@@ -0,0 +1,18 @@
+.flowPath {
+  stroke: #b1b1b7;
+  stroke-width: 1;
+}
+.addIcon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px;
+  :global(.ant-btn) {
+    transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+    // &:visited {
+    //   // color: #40a9ff;
+    //   // box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+    //   transform: scale(1.3);
+    // }
+  }
+}

+ 119 - 0
src/components/Flow/src/edges/BaseEdge/index.tsx

@@ -0,0 +1,119 @@
+import { BranchesOutlined, PlusOutlined, SolutionOutlined } from '@ant-design/icons'
+import { Button, Popover } from 'antd'
+import React, { useMemo, useState } from 'react'
+import type { EdgeProps } from 'react-flow-renderer'
+import { getBezierPath, getEdgeCenter, getMarkerEnd, useStoreState } from 'react-flow-renderer'
+import { getEdgeParams } from '../../utils'
+import styles from './index.less'
+const foreignObjectSize = 50
+import 'antd/lib/button/style/css'
+
+export function CommonEdge(props: EdgeProps) {
+  const {
+    id,
+    sourceX,
+    sourceY,
+    targetX,
+    targetY,
+    source,
+    target,
+    style = {},
+    arrowHeadType,
+    markerEndId
+  } = props
+
+  const [visible, setVisible] = useState(false)
+
+  const nodes = useStoreState(store => store.nodes)
+  const markerEnd = getMarkerEnd(arrowHeadType, markerEndId)
+
+  const sourceNode = useMemo(() => nodes.find(n => n.id === source), [source, nodes])
+  const targetNode = useMemo(() => nodes.find(n => n.id === target), [target, nodes])
+
+  if (!sourceNode || !targetNode) {
+    return null
+  }
+
+  const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
+
+  const d = getBezierPath({
+    sourceX: sx,
+    sourceY: sy - 2,
+    sourcePosition: sourcePos,
+    targetPosition: targetPos,
+    targetX: tx,
+    targetY: ty
+  })
+  const [edgeCenterX, edgeCenterY] = getEdgeCenter({
+    sourceX,
+    sourceY,
+    targetX,
+    targetY
+  })
+
+  const togglePopver = (isShow: boolean) => {
+    setVisible(isShow)
+  }
+
+  const onEdgeClick = () => {
+    togglePopver(true)
+    // evt.stopPropagation()
+    // alert(`remove ${id}`)
+  }
+
+  const addAuditor = async () => {
+    togglePopver(false)
+    setTimeout(() => {
+      flowInstance?.fitView()
+    }, 80)
+  }
+
+  return (
+    <>
+      <path id={id} style={style} className={styles.flowPath} d={d} markerEnd={markerEnd} />
+      <foreignObject
+        width={foreignObjectSize}
+        height={foreignObjectSize}
+        x={edgeCenterX - foreignObjectSize / 2}
+        y={edgeCenterY - foreignObjectSize / 2}>
+        <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">
+                <li>
+                  <Button type="dashed" onClick={addAuditor} icon={<SolutionOutlined />}>
+                    审批人
+                  </Button>
+                </li>
+                <li className="mt-2 condition">
+                  <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
+                    条件分支
+                  </Button>
+                </li>
+              </ul>
+            }
+            trigger="click"
+            placement="right"
+            visible={visible}
+            destroyTooltipOnHide
+            onVisibleChange={togglePopver}
+            overlayClassName="flow-popover">
+            <Button
+              icon={<PlusOutlined />}
+              shape="circle"
+              size="small"
+              onClick={event => onEdgeClick(event, id)}
+            />
+          </Popover>
+        </div>
+      </foreignObject>
+    </>
+  )
+}

+ 4 - 2
src/components/Flow/src/enum/index.ts

@@ -1,10 +1,12 @@
 export enum NodeType {
   /** 标准节点 */
   COMMON = 'common',
-  START = 'start',
-  END = 'end',
+  INPUT = 'start',
+  OUTPUT = 'end',
   /** 操作节点 */
   OPERATION = 'operation',
+  /** 条件按钮节点 */
+  OPERATION_BTN = 'operation_btn',
   /** 分支区块 */
   BLOCK = 'block',
   /** 条件节点 */

+ 218 - 151
src/components/Flow/src/shared/transformer.ts

@@ -1,208 +1,275 @@
 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 { omit } from 'lodash'
+import {
+  ConnectionLineType,
+  Edge,
+  Elements,
+  FlowElement,
+  isEdge,
+  Node,
+  XYPosition
+} from 'react-flow-renderer'
+import { NodeType } from '../enum'
 import { FlowNode, TransformOptions, ComposeOptions } from '../type'
+import { nodeReat } from '../types/node'
 
-/** 返回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)
-}
+const normalNodeWidth = 200
+const normalNodeHeight = 100
 
 /**
  * 传入两个node组合生成新的elments
- * @param connectedNode 上一个node节点
+ * @param sourceNode 上一个node节点
  * @param composeNode 需要组合的节点
  * @param options 额外配置参数
  */
-const composeElement = (connectedNode: Node, composeNode: Node, options: ComposeOptions): FlowElement[] => {
-  const { space, order, offset = 0, direction } = options
+const composeElement = (sourceNode: Node, composeNode: Node, options: ComposeOptions): FlowElement[] => {
+  const { space, order = 0, offset = 0, direction } = options
   // 组合好的node
-  const composedNode: Node = { ...omit(composeNode, ['children', 'order']) }
-  // 根据level,order计算postition
+  const composedNode: Node = {
+    ...omit(composeNode, ['children', 'order'])
+  }
+  // 根据offset计算postition
   const position: XYPosition = {}
+  const realHeight = nodeReat[composeNode.type]?.height || normalNodeHeight
+  const realWidth = nodeReat[composeNode.type]?.width || normalNodeWidth
+
   if (direction === 'x') {
-    position.x = (order + 1) * space
-    position.y = 400 + offset
+    position.x = order * space - realWidth / 2
+    position.y = realHeight === normalNodeHeight ? 0 : normalNodeHeight / 2 - realHeight / 2
+    // if (nodeReat[sourceNode.type]?.width && nodeReat[sourceNode.type]?.width < realWidth) {
+    //   position.x = position.x + (realWidth - nodeReat[sourceNode.type]?.width)
+    // }
   } else {
-    position.x = 400 + offset
-    position.y = (order + 1) * space
-    // TODO: 这里要考虑不同level的x的位置
+    position.x = realWidth === normalNodeWidth ? 0 : normalNodeWidth / 2 - realWidth / 2
+    // 当前的位移量等于上一个的坐标+上一个node节点的高度+偏移量
+    // console.log(
+    //   '当前id:',
+    //   composeNode.id,
+    //   '上一个的坐标:',
+    //   sourceNode?.position?.y || 0,
+    //   '上一个的高度:',
+    //   nodeReat[sourceNode?.type]?.height || 0
+    // )
+    position.y =
+      ((sourceNode?.position?.y && sourceNode?.position?.y + space) || nodeReat[sourceNode.type]?.height) +
+      nodeReat[sourceNode.type]?.height
+    if (sourceNode.type === NodeType.INPUT && composeNode.type !== NodeType.INPUT) {
+      position.y = position.y + nodeReat[sourceNode.type]?.height
+    }
+    // console.log('计算后位置:', position.y)
+    // console.log('=======================')
+  }
+
+  if (offset !== 0) {
+    if (direction === 'x') {
+      position.y = offset + position.y + (offset < 0 ? -realHeight : realHeight)
+    } else {
+      position.x = offset + position.x + (offset < 0 ? -realWidth : realWidth)
+    }
   }
   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
+    source: options?.reverse ? composeNode.id : sourceNode?.id,
+    target: options?.reverse ? sourceNode.id : composeNode.id,
+    type: options.type ?? ConnectionLineType.SmoothStep
   }
   return [composedNode, edge]
 }
-
 /** 通过传入nodes,构建edges */
 export function transformElements<T>(
   nodes: FlowNode<T>[],
-  options: TransformOptions = { space: 200, direction: 'y' }
+  options: TransformOptions = { space: 50, direction: 'y' }
 ): Elements<T> {
-  const firstNode = { id: 'start', type: 'start', position: { x: 400, y: 0 } }
+  const firstNode = { id: 'start', type: 'start' }
   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>
+    opts: { offset?: number; 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)
-      }
+    const { offset, start, end } = opts || {}
+    nodes.forEach((node, idx) => {
       // 通用类型
-      if (item.type === NodeType.COMMON) {
+      if (
+        [
+          NodeType.COMMON,
+          NodeType.CONDITION,
+          NodeType.OPERATION_BTN,
+          NodeType.INPUT,
+          NodeType.OUTPUT
+        ].includes(node.type)
+      ) {
         // 1. 是否开头
         if (idx === 0) {
-          const [element, edge] = composeElement(start ?? firstNode, item, {
-            offset,
-            order: baseOrder + (item.order || idx),
-            space: options.space,
-            direction: options.direction
-          })
+          if (!start) {
+            const [element] = composeElement(firstNode, node, {
+              offset,
+              space: options.space,
+              direction: options.direction
+            })
+            elements.push(element)
+            return
+          }
+          const [element, edge] = composeElement(
+            elements.find(item => item.id === start.id),
+            node,
+            {
+              offset,
+              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
-          })
+          const [element, edge] = composeElement(
+            elements.find(item => item.id === nodes[idx - 1].id),
+            node,
+            {
+              offset,
+              space: options.space,
+              direction: options.direction
+            }
+          )
           elements.push(element, edge)
+
+          // end存在说明是条件节点的结束,则要生成指向end的edges
+          if (end) {
+            const [, lastEdge] = composeElement(end, node, {
+              reverse: true,
+              offset,
+              space: options.space,
+              direction: options.direction
+            })
+            elements.push(lastEdge)
+          }
         } else {
           // 3. 中间
-          const [element, edge] = composeElement(nodes[nodes.length - 1 - idx], item, {
-            offset,
-            order: baseOrder + (item.order || idx),
+          if (nodes[idx - 1].type === NodeType.OPERATION) {
+            // source指向该节点的所有的node节点
+            const srouceNodeIds = elements
+              .filter(isEdge)
+              .filter(edge => edge.target === node.id)
+              .map(edge => edge.source)
+            // 主轴数值最大的source节点
+            const sourcePositionNode = srouceNodeIds.reduce((prev, curr) => {
+              const currNode: Node = elements.find(item => item.id === curr)
+              // 一开始返回该元素本身
+              if (!prev) return currNode
+              if (currNode?.position?.y > prev?.position.y) {
+                return currNode
+              }
+              return prev
+            }, null)
+
+            const [element] = composeElement(sourcePositionNode, node, {
+              reverse: true,
+              offset,
+              space: options.space,
+              direction: options.direction
+            })
+            elements.push(element)
+          } else {
+            const [element, edge] = composeElement(
+              elements.find(item => item.id === nodes[idx - 1].id),
+              node,
+              {
+                offset,
+                space: options.space,
+                direction: options.direction
+              }
+            )
+            elements.push(element, edge)
+          }
+        }
+      } else if (node.type === NodeType.OPERATION) {
+        const [element, edge] = composeElement(
+          elements.find(item => item.id === nodes[idx - 1].id),
+          node,
+          {
+            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) {
+        return generateElements(node?.children || [], { start: element, end: nodes[idx + 1] })
+      } else {
+        // node.type === NodeType.BLOCK
         // 区域节点, 该节点只是占位,直接遍历子节点
         const len = nodes.length // 一共有几个条件分支
         const intermediation = len / 2
         // 一次算出offset,因为该区域节点的offset都是一致的
-        let offset
+        let offset = 0
         if (len % 2 === 0) {
-          if (idx < intermediation) {
-            // 算出的当前offset,传入直接算出偏移的x/y
-            offset = (idx - intermediation) * 50 + 25
-          } else {
-            offset = (idx - intermediation) * 50 - 25
-          }
+          // 算出的当前offset,传入直接算出偏移的x/y
+          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
-        )
+        return generateElements(node.children || [], { offset, start, end })
       }
     })
   }
-  generateElements(nodes)
+  generateElements([firstNode, ...nodes, lastNode])
 
-  return [firstNode, ...elements, { ...lastNode, position: { x: 400, y: (lastOrder + 1) * 200 } }]
+  return elements
 }
 
-// 点击添加条件
-// 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'
-//   }
-// ]
+export const mockNodes = [
+  {
+    id: 'a',
+    type: 'common'
+  },
+  {
+    id: 'b',
+    name: '添加条件', // 用type可替代
+    type: 'operation',
+    children: [
+      {
+        id: 'b-1',
+        type: 'block',
+        children: [
+          {
+            id: 'b-1-1',
+            type: 'condition'
+          },
+          // 正常的节点数据
+          {
+            id: 'b-1-2',
+            type: 'common'
+          }
+        ]
+      },
+      {
+        id: 'b-2',
+        type: 'block',
+        children: [
+          {
+            id: 'b-2-1',
+            type: 'condition'
+          },
+          {
+            id: 'b-2-2',
+            type: 'common'
+          },
+          {
+            id: 'b-2-3',
+            type: 'common'
+          }
+        ]
+      }
+    ]
+  },
+  // {
+  //   id: 'c',
+  //   type: 'operation_btn'
+  // },
+  {
+    id: 'd',
+    type: 'common'
+  }
+]

+ 0 - 0
src/components/Flow/src/types/general.ts


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


+ 28 - 0
src/components/Flow/src/types/node.ts

@@ -0,0 +1,28 @@
+import { NodeType } from '../enum'
+
+export const nodeReat = {
+  [NodeType.COMMON]: {
+    width: 200,
+    height: 100
+  },
+  [NodeType.CONDITION]: {
+    width: 200,
+    height: 100
+  },
+  [NodeType.OPERATION]: {
+    width: 90,
+    height: 32
+  },
+  [NodeType.OPERATION_BTN]: {
+    width: 32,
+    height: 32
+  },
+  [NodeType.INPUT]: {
+    width: 200,
+    height: 36
+  },
+  [NodeType.OUTPUT]: {
+    width: 200,
+    height: 36
+  }
+}

+ 0 - 0
src/components/Flow/src/types/utils.ts


+ 0 - 25
src/components/HeaderSearch/index.less

@@ -1,25 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.headerSearch {
-  display: inline-flex;
-  align-items: center;
-  .input {
-    width: 0;
-    min-width: 0;
-    overflow: hidden;
-    background: transparent;
-    border-radius: 0;
-    transition: width 0.3s, margin-left 0.3s;
-    :global(.ant-select-selection) {
-      background: transparent;
-    }
-    input {
-      box-shadow: none !important;
-    }
-
-    &.show {
-      width: 210px;
-      margin-left: 8px;
-    }
-  }
-}

+ 0 - 101
src/components/HeaderSearch/index.tsx

@@ -1,101 +0,0 @@
-import { SearchOutlined } from '@ant-design/icons';
-import { AutoComplete, Input } from 'antd';
-import useMergedState from 'rc-util/es/hooks/useMergedState';
-import type { AutoCompleteProps } from 'antd/es/auto-complete';
-import React, { useRef } from 'react';
-
-import classNames from 'classnames';
-import styles from './index.less';
-
-export type HeaderSearchProps = {
-  onSearch?: (value?: string) => void;
-  onChange?: (value?: string) => void;
-  onVisibleChange?: (b: boolean) => void;
-  className?: string;
-  placeholder?: string;
-  options: AutoCompleteProps['options'];
-  defaultVisible?: boolean;
-  visible?: boolean;
-  defaultValue?: string;
-  value?: string;
-};
-
-const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
-  const {
-    className,
-    defaultValue,
-    onVisibleChange,
-    placeholder,
-    visible,
-    defaultVisible,
-    ...restProps
-  } = props;
-
-  const inputRef = useRef<Input | null>(null);
-
-  const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
-    value: props.value,
-    onChange: props.onChange,
-  });
-
-  const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
-    value: props.visible,
-    onChange: onVisibleChange,
-  });
-
-  const inputClass = classNames(styles.input, {
-    [styles.show]: searchMode,
-  });
-  return (
-    <div
-      className={classNames(className, styles.headerSearch)}
-      onClick={() => {
-        setSearchMode(true);
-        if (searchMode && inputRef.current) {
-          inputRef.current.focus();
-        }
-      }}
-      onTransitionEnd={({ propertyName }) => {
-        if (propertyName === 'width' && !searchMode) {
-          if (onVisibleChange) {
-            onVisibleChange(searchMode);
-          }
-        }
-      }}
-    >
-      <SearchOutlined
-        key="Icon"
-        style={{
-          cursor: 'pointer',
-        }}
-      />
-      <AutoComplete
-        key="AutoComplete"
-        className={inputClass}
-        value={value}
-        options={restProps.options}
-        onChange={setValue}
-      >
-        <Input
-          size="small"
-          ref={inputRef}
-          defaultValue={defaultValue}
-          aria-label={placeholder}
-          placeholder={placeholder}
-          onKeyDown={(e) => {
-            if (e.key === 'Enter') {
-              if (restProps.onSearch) {
-                restProps.onSearch(value);
-              }
-            }
-          }}
-          onBlur={() => {
-            setSearchMode(false);
-          }}
-        />
-      </AutoComplete>
-    </div>
-  );
-};
-
-export default HeaderSearch;

+ 89 - 0
src/pages/Business/Step/components/Flow/components/Edge/BaseEdge.tsx

@@ -0,0 +1,89 @@
+import React, { useContext, useState } from 'react'
+import { EdgeProps } from 'react-flow-renderer'
+import { Button, Popover } from 'antd'
+import { BranchesOutlined, PlusOutlined, SolutionOutlined } from '@ant-design/icons'
+import styles from './index.less'
+import { FlowContext } from '../../context'
+import { NodeType } from '@/components/Flow/src/enum'
+export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerEnd'> & {
+  centerX: number
+  centerY: number
+  path: string
+}
+const foreignObjectSize = 48
+
+export default ({ id, source, path, centerX, centerY, style }: BaseEdgeProps) => {
+  const [open, setOpen] = useState(false)
+  const { flowStore } = useContext(FlowContext)
+
+  const showForeignObject = flowStore.elements.find(item => item.id === source)?.type !== NodeType.OPERATION
+  const togglePopver = (open: boolean) => {
+    setOpen(open)
+  }
+
+  const onEdgeClick = () => {
+    togglePopver(true)
+    // evt.stopPropagation()
+    // alert(`remove ${id}`)
+  }
+
+  const addAuditor = async () => {
+    togglePopver(false)
+    setTimeout(() => {
+      flowInstance?.fitView()
+    }, 80)
+  }
+  return (
+    <>
+      <path style={style} d={path} className="react-flow__edge-path" />
+      {showForeignObject && (
+        <foreignObject
+          width={foreignObjectSize}
+          height={foreignObjectSize}
+          x={centerX - foreignObjectSize / 2}
+          y={centerY - foreignObjectSize / 2}
+          className={styles.edgebuttonForeignobject}>
+          <body>
+            <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">
+                    <li>
+                      <Button type="dashed" onClick={addAuditor} icon={<SolutionOutlined />}>
+                        审批人
+                      </Button>
+                    </li>
+                    <li className="mt-2 condition">
+                      <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
+                        条件分支
+                      </Button>
+                    </li>
+                  </ul>
+                }
+                trigger="click"
+                placement="right"
+                open={open}
+                destroyTooltipOnHide
+                onOpenChange={togglePopver}
+                overlayClassName="flow-popover">
+                <Button
+                  icon={<PlusOutlined />}
+                  shape="circle"
+                  size="small"
+                  onClick={event => onEdgeClick(event, id)}
+                />
+              </Popover>
+            </div>
+          </body>
+        </foreignObject>
+      )}
+    </>
+  )
+}

+ 69 - 0
src/pages/Business/Step/components/Flow/components/Edge/EdgeText.tsx

@@ -0,0 +1,69 @@
+import React, { memo, useRef, useState, useEffect, FC, PropsWithChildren } from 'react'
+import cc from 'classnames'
+import { EdgeTextProps, Rect } from 'react-flow-renderer'
+
+const EdgeText: FC<PropsWithChildren<EdgeTextProps>> = ({
+  x,
+  y,
+  label,
+  labelStyle = {},
+  labelShowBg = true,
+  labelBgStyle = {},
+  labelBgPadding = [2, 4],
+  labelBgBorderRadius = 2,
+  children,
+  className,
+  ...rest
+}) => {
+  const edgeRef = useRef<SVGTextElement>(null)
+  const [edgeTextBbox, setEdgeTextBbox] = useState<Rect>({ x: 0, y: 0, width: 0, height: 0 })
+  const edgeTextClasses = cc(['react-flow-edge-textwrapper', className || ''])
+
+  useEffect(() => {
+    if (edgeRef.current) {
+      const textBbox = edgeRef.current.getBBox()
+
+      setEdgeTextBbox({
+        x: textBbox.x,
+        y: textBbox.y,
+        width: textBbox.width,
+        height: textBbox.height
+      })
+    }
+  }, [label])
+
+  if (typeof label === 'undefined' || !label) {
+    return null
+  }
+
+  return (
+    <g
+      transform={`translate(${x - edgeTextBbox.width / 2} ${y - edgeTextBbox.height / 2})`}
+      className={edgeTextClasses}
+      {...rest}>
+      {labelShowBg && (
+        <rect
+          width={edgeTextBbox.width + 2 * labelBgPadding[0]}
+          x={-labelBgPadding[0]}
+          y={-labelBgPadding[1]}
+          height={edgeTextBbox.height + 2 * labelBgPadding[1]}
+          className="react-flow-edge-textbg"
+          style={labelBgStyle}
+          rx={labelBgBorderRadius}
+          ry={labelBgBorderRadius}
+        />
+      )}
+      {/* <EdgeWithButton /> */}
+      <text
+        className="react-flow-edge-text"
+        y={edgeTextBbox.height / 2}
+        dy="0.3em"
+        ref={edgeRef}
+        style={labelStyle}>
+        {label}
+      </text>
+      {children}
+    </g>
+  )
+}
+export default memo(EdgeText)

+ 66 - 0
src/pages/Business/Step/components/Flow/components/Edge/EdgeWithButton.tsx

@@ -0,0 +1,66 @@
+import { BranchesOutlined, PlusOutlined, SolutionOutlined } from '@ant-design/icons'
+import { Button, Popover } from 'antd'
+import React, { useState } from 'react'
+import styles from './index.less'
+import 'antd/lib/button/style/css'
+
+export default function EdgeWithButton() {
+  const [open, setOpen] = useState(false)
+
+  const togglePopver = (open: boolean) => {
+    setOpen(open)
+  }
+
+  const onEdgeClick = () => {
+    togglePopver(true)
+    // evt.stopPropagation()
+    // alert(`remove ${id}`)
+  }
+
+  const addAuditor = async () => {
+    togglePopver(false)
+    setTimeout(() => {
+      flowInstance?.fitView()
+    }, 80)
+  }
+
+  return (
+    <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">
+            <li>
+              <Button type="dashed" onClick={addAuditor} icon={<SolutionOutlined />}>
+                审批人
+              </Button>
+            </li>
+            <li className="mt-2 condition">
+              <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
+                条件分支
+              </Button>
+            </li>
+          </ul>
+        }
+        trigger="click"
+        placement="right"
+        open={open}
+        destroyTooltipOnHide
+        onOpenChange={togglePopver}
+        overlayClassName="flow-popover">
+        <Button
+          icon={<PlusOutlined />}
+          shape="circle"
+          size="small"
+          onClick={event => onEdgeClick(event, id)}
+        />
+      </Popover>
+    </div>
+  )
+}

+ 222 - 0
src/pages/Business/Step/components/Flow/components/Edge/SmoothStepEdge.tsx

@@ -0,0 +1,222 @@
+import React, { memo } from 'react'
+
+import { getCenter } from './utils'
+import BaseEdge from './BaseEdge'
+import { EdgeSmoothStepProps, Position } from 'react-flow-renderer'
+// import { nodeReat } from '@/components/Flow/src/types/node'
+
+// These are some helper methods for drawing the round corners
+// The name indicates the direction of the path. "bottomLeftCorner" goes
+// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
+// We have to consider the direction of the paths because of the animated lines.
+const bottomLeftCorner = (x: number, y: number, size: number): string =>
+  `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
+const leftBottomCorner = (x: number, y: number, size: number): string =>
+  `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
+const bottomRightCorner = (x: number, y: number, size: number): string =>
+  `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
+const rightBottomCorner = (x: number, y: number, size: number): string =>
+  `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
+const leftTopCorner = (x: number, y: number, size: number): string =>
+  `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
+const topLeftCorner = (x: number, y: number, size: number): string =>
+  `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
+const topRightCorner = (x: number, y: number, size: number): string =>
+  `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
+const rightTopCorner = (x: number, y: number, size: number): string =>
+  `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
+
+export interface GetSmoothStepPathParams {
+  sourceX: number
+  sourceY: number
+  sourcePosition?: Position
+  targetX: number
+  targetY: number
+  targetPosition?: Position
+  borderRadius?: number
+  centerX?: number
+  centerY?: number
+  resizeOffset?: number
+}
+
+export function getSmoothStepPath({
+  sourceX,
+  sourceY,
+  sourcePosition = Position.Bottom,
+  targetX,
+  targetY,
+  targetPosition = Position.Top,
+  borderRadius = 5,
+  centerX,
+  centerY
+}: // resizeYOffset = 0
+GetSmoothStepPathParams): string {
+  const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY })
+  const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX))
+  const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY))
+  const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY)
+  const leftAndRight = [Position.Left, Position.Right]
+  const cX = typeof centerX !== 'undefined' ? centerX : _centerX
+  const cY = typeof centerY !== 'undefined' ? centerY : _centerY
+
+  let firstCornerPath = null
+  let secondCornerPath = null
+
+  // console.log(resizeYOffset)
+
+  if (sourceX <= targetX) {
+    firstCornerPath =
+      sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize)
+
+    // console.log(firstCornerPath)
+
+    secondCornerPath =
+      sourceY <= targetY
+        ? rightTopCorner(targetX, cY, cornerSize)
+        : rightBottomCorner(targetX, cY, cornerSize)
+
+    // console.log(secondCornerPath)
+  } else {
+    firstCornerPath =
+      sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize)
+    secondCornerPath =
+      sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize)
+  }
+
+  if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
+    if (sourceX <= targetX) {
+      firstCornerPath =
+        sourceY <= targetY
+          ? rightTopCorner(cX, sourceY, cornerSize)
+          : rightBottomCorner(cX, sourceY, cornerSize)
+      secondCornerPath =
+        sourceY <= targetY
+          ? bottomLeftCorner(cX, targetY, cornerSize)
+          : topLeftCorner(cX, targetY, cornerSize)
+    } else if (
+      (sourcePosition === Position.Right && targetPosition === Position.Left) ||
+      (sourcePosition === Position.Left && targetPosition === Position.Right) ||
+      (sourcePosition === Position.Left && targetPosition === Position.Left)
+    ) {
+      // and sourceX > targetX
+      firstCornerPath =
+        sourceY <= targetY
+          ? leftTopCorner(cX, sourceY, cornerSize)
+          : leftBottomCorner(cX, sourceY, cornerSize)
+      secondCornerPath =
+        sourceY <= targetY
+          ? bottomRightCorner(cX, targetY, cornerSize)
+          : topRightCorner(cX, targetY, cornerSize)
+    }
+  } else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
+    if (sourceX <= targetX) {
+      firstCornerPath =
+        sourceY <= targetY
+          ? rightTopCorner(targetX, sourceY, cornerSize)
+          : rightBottomCorner(targetX, sourceY, cornerSize)
+    } else {
+      firstCornerPath =
+        sourceY <= targetY
+          ? leftTopCorner(targetX, sourceY, cornerSize)
+          : leftBottomCorner(targetX, sourceY, cornerSize)
+    }
+    secondCornerPath = ''
+  } else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
+    if (sourceX <= targetX) {
+      firstCornerPath =
+        sourceY <= targetY
+          ? bottomLeftCorner(sourceX, targetY, cornerSize)
+          : topLeftCorner(sourceX, targetY, cornerSize)
+    } else {
+      firstCornerPath =
+        sourceY <= targetY
+          ? bottomRightCorner(sourceX, targetY, cornerSize)
+          : topRightCorner(sourceX, targetY, cornerSize)
+    }
+    secondCornerPath = ''
+  }
+
+  return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`
+}
+
+export default memo(
+  ({
+    id,
+    source,
+    sourceX,
+    sourceY,
+    targetX,
+    targetY,
+    label,
+    labelStyle,
+    labelShowBg,
+    labelBgStyle,
+    labelBgPadding,
+    labelBgBorderRadius,
+    style,
+    sourcePosition = Position.Bottom,
+    targetPosition = Position.Top,
+    borderRadius = 5
+  }: EdgeSmoothStepProps) => {
+    // const { nodes, edges } = useStore().getState()
+    // // 需要修正的高度
+    // let resizeYOffset = 0
+
+    // if (source === 'b-1-2') {
+    //   // 过滤找到指向同一个target的edge数组,包括了自己
+    //   const relationalEdges = edges.filter(item => item.target === target)
+    //   if (relationalEdges.length > 1) {
+    //     const nodeIds = relationalEdges.map(item => item.source)
+    //     const relationalNodes = nodes.filter(item => nodeIds.includes(item.id))
+    //     const currentNode = relationalNodes.find(item => item.id === source)
+    //     relationalNodes.forEach(item => {
+    //       if (item.position.y > currentNode?.position.y) {
+    //         resizeYOffset = item.position.y - (currentNode?.position.y || 0) - nodeReat[item.type]?.height / 2
+    //       }
+    //     })
+    //     console.log('resizeHeightOffset', resizeYOffset)
+    //     // console.log(centerX, centerY)
+    //   }
+    // }
+    const [centerX, centerY] = getCenter({
+      sourceX,
+      sourceY,
+      targetX,
+      targetY,
+      sourcePosition,
+      targetPosition
+    })
+    // if (source === 'b-1-2') {
+    //   console.log('resizeOffset', resizeYOffset)
+
+    //   debugger
+    // }
+    const path = getSmoothStepPath({
+      sourceX,
+      sourceY,
+      sourcePosition,
+      targetX,
+      targetY,
+      targetPosition,
+      borderRadius
+      // resizeYOffset
+    })
+
+    return (
+      <BaseEdge
+        id={id}
+        source={source}
+        path={path}
+        centerX={centerX}
+        centerY={centerY}
+        label={label}
+        labelStyle={labelStyle}
+        labelShowBg={labelShowBg}
+        labelBgStyle={labelBgStyle}
+        labelBgPadding={labelBgPadding}
+        labelBgBorderRadius={labelBgBorderRadius}
+        style={style}
+      />
+    )
+  }
+)

+ 8 - 1
src/pages/Business/Step/components/Flow/components/Edge/index.less

@@ -6,7 +6,6 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  padding: 10px;
   :global(.ant-btn) {
     transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
     // &:visited {
@@ -16,3 +15,11 @@
     // }
   }
 }
+.edgebuttonForeignobject {
+  & > body {
+    background: transparent;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}

+ 1 - 0
src/pages/Business/Step/components/Flow/components/Edge/index.ts

@@ -0,0 +1 @@
+export { default as SmoothStepEdge } from './SmoothStepEdge'

+ 0 - 170
src/pages/Business/Step/components/Flow/components/Edge/index.tsx

@@ -1,170 +0,0 @@
-import { buildUUID } from '@/utils/uuid'
-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'
-import { getBezierPath, getEdgeCenter, getMarkerEnd, useStoreState } from 'react-flow-renderer'
-import { Actions, eId, FlowContext } from '../../context'
-import { generateElements, genreateElementEnum, getEdgeParams } from '../../utils'
-import { addApprovalNode } from '@/services/api/project'
-import styles from './index.less'
-import 'antd/lib/button/style/css'
-import consts from '@/utils/consts'
-import useLoading from '../../hooks/useLoading'
-import { ConfigureType, SectorType } from '../../enum'
-const foreignObjectSize = 50
-
-export function CommonEdge(props: EdgeProps) {
-  const {
-    id,
-    sourceX,
-    sourceY,
-    targetX,
-    targetY,
-    source,
-    target,
-    style = {},
-    arrowHeadType,
-    markerEndId
-  } = props
-
-  const [visible, setVisible] = useState(false)
-  const { flowState, dispatch } = useContext(FlowContext)
-  const { elements, flowInstance, flowData, dataID, readPretty } = flowState
-  const { run: showLoading } = useLoading()
-  const setElements = els => {
-    dispatch({
-      type: Actions.SET_ELEMENTS,
-      payload: els
-    })
-  }
-  const showBtn =
-    flowData.get(source) && target === eId
-      ? flowData.get(source)?.accounts.every(item => !item.configure.includes(ConfigureType.NEXTSECTOR))
-      : true
-
-  const nodes = useStoreState(store => store.nodes)
-  const markerEnd = getMarkerEnd(arrowHeadType, markerEndId)
-
-  const sourceNode = useMemo(() => nodes.find(n => n.id === source), [source, nodes])
-  const targetNode = useMemo(() => nodes.find(n => n.id === target), [target, nodes])
-
-  if (!sourceNode || !targetNode) {
-    return null
-  }
-
-  const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
-
-  const d = getBezierPath({
-    sourceX: sx,
-    sourceY: sy - 2,
-    sourcePosition: sourcePos,
-    targetPosition: targetPos,
-    targetX: tx,
-    targetY: ty
-  })
-  const [edgeCenterX, edgeCenterY] = getEdgeCenter({
-    sourceX,
-    sourceY,
-    targetX,
-    targetY
-  })
-
-  const togglePopver = (isShow: boolean) => {
-    setVisible(isShow)
-  }
-
-  const onEdgeClick = () => {
-    togglePopver(true)
-    // evt.stopPropagation()
-    // alert(`remove ${id}`)
-  }
-
-  const addAuditor = async () => {
-    const uid = buildUUID()
-    const newNode = {
-      id: uid,
-      type: 'input',
-      data: {}
-    }
-    const currentEdge = elements.find(item => item.id === id)
-    const newElements = generateElements(genreateElementEnum.ADD, elements)(newNode, currentEdge)
-
-    const { code = -1 } = await addApprovalNode({
-      ID: dataID,
-      segmentBrotherID: source,
-      flowProcess: newElements,
-      segment: {
-        ID: uid,
-        name: '审批人',
-        sectorType: SectorType.APPROVAL
-      }
-    })
-    if (code === consts.RET_CODE.SUCCESS) {
-      setElements(newElements)
-      dispatch({
-        type: Actions.SET_FLOW_NODE,
-        payload: {
-          id: uid,
-          node: null
-        }
-      })
-      togglePopver(false)
-      setTimeout(() => {
-        showLoading()
-        flowInstance?.fitView()
-      }, 80)
-    }
-  }
-
-  return (
-    <>
-      <path id={id} style={style} className={styles.flowPath} d={d} markerEnd={markerEnd} />
-      {showBtn && !readPretty && (
-        <foreignObject
-          width={foreignObjectSize}
-          height={foreignObjectSize}
-          x={edgeCenterX - foreignObjectSize / 2}
-          y={edgeCenterY - foreignObjectSize / 2}>
-          <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">
-                  <li>
-                    <Button type="dashed" onClick={addAuditor} icon={<SolutionOutlined />}>
-                      审批人
-                    </Button>
-                  </li>
-                  <li className="mt-2 condition">
-                    <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
-                      条件分支
-                    </Button>
-                  </li>
-                </ul>
-              }
-              trigger="click"
-              placement="right"
-              visible={visible}
-              destroyTooltipOnHide
-              onVisibleChange={togglePopver}
-              overlayClassName="flow-popover">
-              <Button
-                icon={<PlusOutlined />}
-                shape="circle"
-                size="small"
-                onClick={event => onEdgeClick(event, id)}
-              />
-            </Popover>
-          </div>
-        </foreignObject>
-      )}
-    </>
-  )
-}

+ 55 - 0
src/pages/Business/Step/components/Flow/components/Edge/utils.ts

@@ -0,0 +1,55 @@
+import { ArrowHeadType, Position } from 'react-flow-renderer'
+
+export const getMarkerEnd = (markerType?: ArrowHeadType, markerEndId?: string): string => {
+  if (typeof markerEndId !== 'undefined' && markerEndId) {
+    return `url(#${markerEndId})`
+  }
+
+  return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none'
+}
+
+export interface GetCenterParams {
+  sourceX: number
+  sourceY: number
+  targetX: number
+  targetY: number
+  sourcePosition?: Position
+  targetPosition?: Position
+}
+
+const LeftOrRight = [Position.Left, Position.Right]
+
+export const getCenter = ({
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+  sourcePosition = Position.Bottom,
+  targetPosition = Position.Top
+}: GetCenterParams): [number, number, number, number] => {
+  const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
+  const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
+
+  // we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
+  // a mixed edge is when one the source is on the left and the target is on the top for example.
+  const mixedEdge =
+    (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight)
+
+  if (mixedEdge) {
+    const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0
+    const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset
+
+    const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY)
+    const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset
+
+    return [centerX, centerY, xOffset, yOffset]
+  }
+
+  const xOffset = Math.abs(targetX - sourceX) / 2
+  const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset
+
+  const yOffset = Math.abs(targetY - sourceY) / 2
+  const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset
+
+  return [centerX, centerY, xOffset, yOffset]
+}

+ 43 - 48
src/pages/Business/Step/components/Flow/components/Graph/index.tsx

@@ -1,72 +1,62 @@
-import React, { useContext, useMemo } from 'react'
-import ReactFlow, { Background, MiniMap, Controls } from 'react-flow-renderer'
+import React, { startTransition, useContext, useMemo } from 'react'
+import ReactFlow, {
+  Background,
+  MiniMap,
+  Controls,
+  OnLoadParams,
+  ConnectionLineType
+} from 'react-flow-renderer'
 import { Actions, FlowContext } from '../../context'
+import type { NodeTypesType, EdgeTypesType } from 'react-flow-renderer'
+import { BaseNode, ConditionNode, ConditionButton, ConditionHanderNode, InputNode, OutputNode } from '../Node'
+import { SmoothStepEdge } from '../Edge'
+import { FlowElements } from '@/components/Flow/src/type'
+import { NodeType } from '@/components/Flow/src/enum/index'
 
-import type { Elements, NodeTypesType, EdgeTypesType } from 'react-flow-renderer'
-import { CommonNode, ConditionHanderNode, InputNode, OutputNode } from '../Node'
-import { CommonEdge } from '../Edge'
-
-const edgeTypes: EdgeTypesType = {
-  common: CommonEdge
+type FlowGrophProps<T> = {
+  defaultElements?: FlowElements<T>
+  elements?: FlowElements<T>
 }
 
-// type flowProcessDataItem = {
-//   approvalType: ApprovalType
-//   approvalMethod: ApprovalMethod
-//   approvalAccounts: { ID: string; name: string }[]
-// }
-
-type FlowGrophProps = {
-  dataID: string
-  flowProcess: Elements
-  readPretty: boolean
-  flowProcessData: API.ApprovalProcessItem[]
-}
-const FlowGroph: React.FC<FlowGrophProps> = ({ dataID, flowProcess, flowProcessData, readPretty }) => {
-  const { flowState, dispatch } = useContext(FlowContext)
+function FlowGroph<T>({ defaultElements, elements }: FlowGrophProps<T>) {
+  const { dispatch } = useContext(FlowContext)
   const nodeTypes: NodeTypesType = useMemo(
     () => ({
-      start: InputNode,
-      end: OutputNode,
-      common: CommonNode,
-      conditionHander: ConditionHanderNode
+      [NodeType.INPUT]: InputNode,
+      [NodeType.OUTPUT]: OutputNode,
+      [NodeType.COMMON]: BaseNode,
+      [NodeType.OPERATION]: ConditionHanderNode,
+      [NodeType.CONDITION]: ConditionNode,
+      [NodeType.OPERATION_BTN]: ConditionButton
     }),
     []
   )
 
-  const { elements } = flowState
+  const edgeTypes: EdgeTypesType = useMemo(
+    () => ({
+      [ConnectionLineType.SmoothStep]: SmoothStepEdge
+    }),
+    []
+  )
 
   const defaultOptions = {
     nodesDraggable: false, // 不可拖拽
     // zoomOnScroll: false, // 使用鼠标滚轮或触控板放大和缩小图形
     zoomOnPinch: false, // 使用捏合放大和缩小图形
     // snapToGrid: true,
-    onLoad: reactFlowInstance => {
+    onLoad: (flowInstance: OnLoadParams) => {
       const payload = {}
-      if (flowProcess?.length) {
-        payload.elements = flowProcess
-        payload.flowData = new Map(
-          Object.entries(
-            flowProcessData.reduce((prev, curr) => {
-              if (curr.ID) {
-                prev[curr.ID] = curr.participantInfo
-              }
-              return prev
-            }, {})
-          )
-        )
+      if (defaultElements?.length || elements?.length) {
+        payload.elements = defaultElements || elements
       }
-      payload.flowInstance = reactFlowInstance
-      payload.dataID = dataID
-      payload.readPretty = readPretty
-
+      payload.flowInstance = flowInstance
       dispatch({
         type: Actions.INIT_FLOW_CONTEXT,
         payload
       })
-      setTimeout(() => {
-        reactFlowInstance?.fitView()
-      }, 0)
+      startTransition(() => {
+        flowInstance?.fitView({ duration: 500 })
+      })
     }
   }
 
@@ -77,7 +67,12 @@ const FlowGroph: React.FC<FlowGrophProps> = ({ dataID, flowProcess, flowProcessD
   }
 
   return (
-    <ReactFlow {...defaultOptions} elements={elements} nodeTypes={nodeTypes} edgeTypes={edgeTypes}>
+    <ReactFlow
+      {...defaultOptions}
+      selectNodesOnDrag={false}
+      elements={defaultElements}
+      nodeTypes={nodeTypes}
+      edgeTypes={edgeTypes}>
       <MiniMap />
       <Background />
       <Controls {...controlsOptions} className="text-xl" />

+ 53 - 0
src/pages/Business/Step/components/Flow/components/Node/BaseNode.tsx

@@ -0,0 +1,53 @@
+import { CloseOutlined, RightOutlined } from '@ant-design/icons'
+import { memo, useContext } from 'react'
+import { Handle } from 'react-flow-renderer'
+import { FlowContext } from '../../context'
+
+export const BaseNode = memo(() => {
+  const { flowStore, dispatch } = useContext(FlowContext)
+  console.log(flowStore, dispatch)
+
+  const removeNode = async () => {
+    // const node = { id, data }
+    // const newElements = generateElements(genreateElementEnum.DEL, flowState.elements)(node)
+    // const { code = -1 } = await removeApprovalNode({
+    //   ID: flowState.dataID,
+    //   segmentID: id,
+    //   flowProcess: newElements
+    // })
+    // if (code === consts.RET_CODE.SUCCESS) {
+    //   dispatch({
+    //     type: Actions.REMOVE_FLOW_NODE,
+    //     payload: { ...node, newElements }
+    //   })
+    //   setTimeout(() => {
+    //     flowInstance?.fitView()
+    //   }, 80)
+    // }
+  }
+
+  return (
+    <>
+      <Handle type="source" position="bottom" />
+      <Handle type="target" position="top" />
+      <Handle type="source" position="right" />
+      <Handle type="source" position="left" />
+      <div className="shadow-card cursor-default p-1 bg-hex-1890ff rounded-lg node-content w-200px max-h-86px">
+        <div className="relative text-light-500  p-1 flex justify-between items-center">
+          <div className="leading-5 h-5 text-12px">审批人</div>
+          <div className="remove-node w-24px h-24px bg-hex-ff4d4f align-middle" onClick={removeNode}>
+            <CloseOutlined size={12} />
+          </div>
+        </div>
+        <div className="text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px flex justify-between">
+          <div>
+            <span className="text-hex-999999">发起人自选</span>
+          </div>
+          <div>
+            <RightOutlined />
+          </div>
+        </div>
+      </div>
+    </>
+  )
+})

+ 58 - 0
src/pages/Business/Step/components/Flow/components/Node/ConditionButton.tsx

@@ -0,0 +1,58 @@
+import { BranchesOutlined, PlusOutlined, SolutionOutlined } from '@ant-design/icons'
+import { Button, Popover } from 'antd'
+import { memo, useState } from 'react'
+import { Handle } from 'react-flow-renderer'
+import styles from '../Edge/index.less'
+export const ConditionButton = memo(() => {
+  const [open, setOpen] = useState(false)
+
+  const togglePopver = (open: boolean) => {
+    setOpen(open)
+  }
+
+  const addAuditor = () => {}
+  return (
+    <>
+      <Handle type="target" position="top" />
+      <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">
+              <li>
+                <Button type="dashed" onClick={addAuditor} icon={<SolutionOutlined />}>
+                  审批人
+                </Button>
+              </li>
+              <li className="mt-2 condition">
+                <Button type="dashed" onClick={addAuditor} icon={<BranchesOutlined />}>
+                  条件分支
+                </Button>
+              </li>
+            </ul>
+          }
+          trigger="click"
+          placement="right"
+          open={open}
+          destroyTooltipOnHide
+          onOpenChange={togglePopver}
+          overlayClassName="flow-popover">
+          <Button
+            icon={<PlusOutlined />}
+            shape="circle"
+            size="small"
+            onClick={event => onEdgeClick(event, id)}
+          />
+        </Popover>
+      </div>
+      <Handle type="source" position="left" />
+      <Handle type="source" position="right" />
+    </>
+  )
+})

+ 14 - 0
src/pages/Business/Step/components/Flow/components/Node/ConditionHanderNode.tsx

@@ -0,0 +1,14 @@
+import { Button } from 'antd'
+import { memo } from 'react'
+import { Handle } from 'react-flow-renderer'
+
+export const ConditionHanderNode = memo(() => {
+  return (
+    <>
+      <Handle type="target" position="top" />
+      <Button shape="round">添加条件</Button>
+      <Handle type="source" position="left" />
+      <Handle type="source" position="right" />
+    </>
+  )
+})

+ 51 - 0
src/pages/Business/Step/components/Flow/components/Node/ConditionNode.tsx

@@ -0,0 +1,51 @@
+import { CloseOutlined, RightOutlined } from '@ant-design/icons'
+import { memo, useContext } from 'react'
+import { Handle } from 'react-flow-renderer'
+import { FlowContext } from '../../context'
+
+export const ConditionNode = memo(() => {
+  const { flowStore, dispatch } = useContext(FlowContext)
+  console.log(flowStore, dispatch)
+
+  const removeNode = async () => {
+    // const node = { id, data }
+    // const newElements = generateElements(genreateElementEnum.DEL, flowState.elements)(node)
+    // const { code = -1 } = await removeApprovalNode({
+    //   ID: flowState.dataID,
+    //   segmentID: id,
+    //   flowProcess: newElements
+    // })
+    // if (code === consts.RET_CODE.SUCCESS) {
+    //   dispatch({
+    //     type: Actions.REMOVE_FLOW_NODE,
+    //     payload: { ...node, newElements }
+    //   })
+    //   setTimeout(() => {
+    //     flowInstance?.fitView()
+    //   }, 80)
+    // }
+  }
+
+  return (
+    <>
+      <Handle type="target" position="top" />
+      <div className="shadow-card cursor-default p-1 bg-white rounded-lg node-content w-200px max-h-86px">
+        <div className="relative text-hex-52C41A  p-1 flex justify-between items-center">
+          <div className="leading-5 h-5 text-12px">审批人</div>
+          <div className="remove-node w-24px h-24px bg-hex-ff4d4f align-middle" onClick={removeNode}>
+            <CloseOutlined size={12} />
+          </div>
+        </div>
+        <div className="text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px flex justify-between">
+          <div>
+            <span className="text-hex-999999">符合xx条件</span>
+          </div>
+          <div>
+            <RightOutlined />
+          </div>
+        </div>
+      </div>
+      <Handle type="source" position="bottom" />
+    </>
+  )
+})

+ 29 - 0
src/pages/Business/Step/components/Flow/components/Node/DefaultNodes.tsx

@@ -0,0 +1,29 @@
+import { Handle } from 'react-flow-renderer'
+import { memo } from 'react'
+export const InputNode = memo(() => {
+  return (
+    <>
+      <div className="shadow-card cursor-default p-1 bg-hex-bfbfbf rounded-lg text-center w-200px h-36px">
+        <div className=" text-hex-fff leading-7 h-7 text-12px ml-1">流程发起人</div>
+        {/* <div className="text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px">
+          具有新建权限的员工
+        </div> */}
+      </div>
+      <Handle type="source" position="bottom" />
+    </>
+  )
+})
+
+export const OutputNode = memo(() => {
+  return (
+    <>
+      <Handle type="target" position="top" />
+      <div className="shadow-card cursor-default p-1 bg-hex-bfbfbf rounded-lg text-center w-200px h-36px">
+        <div className=" text-hex-fff leading-7 h-7 text-12px ml-1">流程结束</div>
+        {/* <div className="text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px">
+          具有新建权限的员工
+        </div> */}
+      </div>
+    </>
+  )
+})

+ 5 - 0
src/pages/Business/Step/components/Flow/components/Node/index.ts

@@ -0,0 +1,5 @@
+export * from './DefaultNodes'
+export * from './BaseNode'
+export * from './ConditionHanderNode'
+export * from './ConditionNode'
+export * from './ConditionButton'

+ 0 - 130
src/pages/Business/Step/components/Flow/components/Node/index.tsx

@@ -1,130 +0,0 @@
-import { Handle } from 'react-flow-renderer'
-import { Actions, FlowContext } from '../../context'
-import { useContext } from 'react'
-import { CloseOutlined, RightOutlined } from '@ant-design/icons'
-import { removeApprovalNode } from '@/services/api/project'
-import consts from '@/utils/consts'
-import { generateElements, genreateElementEnum } from '../../utils'
-import { Button } from 'antd'
-import useLoading from '../../hooks/useLoading'
-import classNames from 'classnames'
-export const InputNode = () => {
-  return (
-    <>
-      <div className="shadow-card cursor-default p-1 bg-hex-bfbfbf rounded-lg text-center">
-        <div className=" text-hex-fff leading-7 h-7 text-12px ml-1">流程发起人</div>
-        {/* <div className="text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px">
-          具有新建权限的员工
-        </div> */}
-      </div>
-      <Handle type="source" position="bottom" />
-    </>
-  )
-}
-
-export const OutputNode = () => {
-  return (
-    <>
-      <Handle type="target" position="top" />
-      <div className="shadow-card cursor-default p-1 bg-hex-bfbfbf rounded-lg text-center">
-        <div className=" text-hex-fff leading-7 h-7 text-12px ml-1">流程结束</div>
-        {/* <div className="text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px">
-          具有新建权限的员工
-        </div> */}
-      </div>
-    </>
-  )
-}
-
-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
-  const { run: showLoading } = useLoading()
-  const removeNode = async () => {
-    const node = { id, data }
-    const newElements = generateElements(genreateElementEnum.DEL, flowState.elements)(node)
-    const { code = -1 } = await removeApprovalNode({
-      ID: flowState.dataID,
-      segmentID: id,
-      flowProcess: newElements
-    })
-    if (code === consts.RET_CODE.SUCCESS) {
-      dispatch({
-        type: Actions.REMOVE_FLOW_NODE,
-        payload: { ...node, newElements }
-      })
-      setTimeout(() => {
-        showLoading()
-        flowInstance?.fitView()
-      }, 80)
-    }
-  }
-  const openDrawer = () => {
-    !readPretty &&
-      dispatch({
-        type: Actions.OPEN_MODAL,
-        payload: {
-          id
-        }
-      })
-  }
-
-  const auditor = flowData?.get(id)
-
-  return (
-    <>
-      <Handle type="target" position="top" />
-      {/* <div className="min-h-18 w-52 node_content">
-        <div className="bg-hex-f78b22 text-light-500 leading-6 h-6 px-4 text-12px rounded-t-sm flex justify-between items-center">
-          <span>审批人</span>
-          <span className="remove_node" onClick={removeNode}>
-            <CloseOutlined size={12} />
-          </span>
-        </div>
-        <div className="p-4 bg-white rounded-b-sm">
-          <div className="text-14px  flex justify-between operate_node">
-            <span>请选择选批人</span>
-            <span>
-              <RightOutlined />
-            </span>
-          </div>
-        </div>
-      </div> */}
-      <div className="shadow-card cursor-default p-1 bg-hex-1890ff rounded-lg node_content">
-        <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}>
-              <CloseOutlined size={12} />
-            </div>
-          ) : null}
-        </div>
-        <div
-          className={classNames(
-            'text-14px px-4px py-5px leading-22px text-black text-opacity-85 bg-white rounded-4px h-32px flex justify-between',
-            { 'cursor-pointer': !readPretty }
-          )}
-          onClick={openDrawer}>
-          <div>
-            {auditor ? auditor.accounts?.[0]?.name : <span className="text-hex-999999">发起人自选</span>}
-          </div>
-          <div>
-            <RightOutlined />
-          </div>
-        </div>
-      </div>
-      <Handle type="source" position="bottom" />
-    </>
-  )
-}

+ 19 - 37
src/pages/Business/Step/components/Flow/context/index.tsx

@@ -1,62 +1,44 @@
-import React, { createContext, useReducer } from 'react'
+import React, { createContext, useReducer, Dispatch } from 'react'
 import reducer from './reducer'
 import { Actions } from '../enum'
 
 import type { Elements, OnLoadParams } from 'react-flow-renderer'
 import { ArrowHeadType } from 'react-flow-renderer'
+import { createUid } from '@/utils/util'
 
-const FlowContext = createContext<Partial<InitialState>>({})
-export type InitialState = {
+export type FlowContextState = {
   elements: Elements
-  flowInstance?: OnLoadParams | null
-  flowData: Map<string, any>
-  /** @name 抽屉控制器 */
-  modalConfig: {
-    visible: boolean
-    nodeType?: string | null
-    nodeId?: string | null
-  }
-  /** @name 只读态 */
-  readPretty?: boolean
-  /** @name 画布loading控制器 */
-  canvasLoading?: boolean
+  flowInstance?: OnLoadParams
 }
 
+const FlowContext = createContext<{ flowStore: FlowContextState; dispatch: Dispatch<Actions> }>({})
+
 export const initialElements = [
   {
-    id: '89e43a46-88da-4243-ad0b-3c701819ff63',
-    type: 'start',
-    position: { x: 500, y: 0 },
-    data: { sort: 0 }
+    id: 'input',
+    type: 'input',
+    position: { x: 0, y: 0 }
   },
   {
-    id: '83282d24-22d9-4bd8-a6bf-71d6df94f29e',
-    type: 'end',
-    position: { x: 500, y: 200 },
-    data: { sort: 1 }
+    id: 'output',
+    type: 'output',
+    position: { x: 0, y: 0 }
   },
   {
-    id: '29b4469f-9522-4688-8a35-a57ca4875a7a',
-    source: '89e43a46-88da-4243-ad0b-3c701819ff63',
-    target: '83282d24-22d9-4bd8-a6bf-71d6df94f29e',
-    type: 'common',
+    id: createUid(),
+    source: 'input',
+    target: 'output',
     arrowHeadType: ArrowHeadType.Arrow
   }
 ]
-const initialState: InitialState = {
-  elements: initialElements,
-  flowData: new Map(),
-  modalConfig: {
-    visible: false
-  }
+const initialState: FlowContextState = {
+  elements: initialElements
 }
 
 const FlowContextProvider = props => {
   const { children } = props
-  const [flowState, dispatch] = useReducer(reducer, initialState)
-  return <FlowContext.Provider value={{ flowState, dispatch }}>{children}</FlowContext.Provider>
+  const [flowStore, dispatch] = useReducer(reducer, initialState)
+  return <FlowContext.Provider value={{ flowStore, dispatch }}>{children}</FlowContext.Provider>
 }
 
-export const sId = '89e43a46-88da-4243-ad0b-3c701819ff63'
-export const eId = '83282d24-22d9-4bd8-a6bf-71d6df94f29e'
 export { FlowContext, FlowContextProvider, Actions }

+ 9 - 8
src/pages/Business/Step/components/Flow/index.less

@@ -1,11 +1,12 @@
+/* stylelint-disable selector-class-pattern */
 .flow-container {
-  & .react-flow-node {
-    width: 200px;
-    max-height: 86px;
-    border-radius: 8px;
-    opacity: 1;
-  }
-  & .react-flow-handle {
+  // & .react-flow__node {
+  //   width: 200px;
+  //   max-height: 86px;
+  //   border-radius: 8px;
+  //   opacity: 1;
+  // }
+  & .react-flow__handle {
     opacity: 0;
   }
   .node-content {
@@ -34,7 +35,7 @@
       opacity: 1;
     }
   }
-  & .react-flow-controls {
+  & .react-flow__controls {
     bottom: 90%;
     left: 90%;
     display: flex;

+ 5 - 37
src/pages/Business/Step/components/Flow/index.tsx

@@ -1,37 +1,14 @@
-import React, { useState, useEffect } from 'react'
 import { ReactFlowProvider } from 'react-flow-renderer'
 import { FlowContextProvider } from './context'
 import FlowGroph from './components/Graph'
-import FlowDrawer from './components/Drawer'
-import { useRequest } from '@umijs/max'
-import { queryApprovalDetail } from '@/services/api/project'
+import { FlowElements } from '@/components/Flow/src/type'
 import './index.less'
-import { LoadingBar } from './components/Toolbar'
 
-type ApprovalFlowProps = {
-  dataID: string
-  readPretty: boolean
+type ApprovalFlowProps<T> = {
+  defaultElements?: FlowElements<T>
 }
 
-const ApprovalFlow: React.FC<ApprovalFlowProps> = ({ readPretty, dataID }) => {
-  const [state, setState] = useState({
-    flowProcess: null,
-    flowProcessData: null
-  })
-  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])
-
-  const { flowProcess, flowProcessData } = state
+function ApprovalFlow<T>({ defaultElements }: ApprovalFlowProps<T>) {
   return (
     <div className="flow-container h-full w-full">
       <FlowContextProvider>
@@ -39,17 +16,8 @@ const ApprovalFlow: React.FC<ApprovalFlowProps> = ({ readPretty, dataID }) => {
           {/** 顶部工具栏 */}
           {/* <ToolBar dataId={props.dataID} closeAnimateContent={props.closeAnimateContent} /> */}
           <div className="h-full w-full">
-            {flowProcess ? (
-              <FlowGroph
-                readPretty={readPretty}
-                dataID={dataID}
-                flowProcess={flowProcess}
-                flowProcessData={flowProcessData}
-              />
-            ) : null}
+            <FlowGroph<T> defaultElements={defaultElements} />
           </div>
-          <LoadingBar />
-          <FlowDrawer />
         </ReactFlowProvider>
       </FlowContextProvider>
     </div>

+ 99 - 1
src/pages/Business/Step/index.tsx

@@ -1,5 +1,103 @@
+import useDrawer from '@/components/Drawer'
+import { mockNodes, transformElements } from '@/components/Flow/src/shared/transformer'
+import { queryApprovalDetail } from '@/services/api/project'
+import consts from '@/utils/consts'
+import { PageContainer } from '@ant-design/pro-layout'
+import ProTable, { ActionType, ProColumns } from '@ant-design/pro-table'
+import { Button } from 'antd'
+import { useMemo, useRef, useState } from 'react'
+import { isNode } from 'react-flow-renderer'
+import LeftMenu from '../RuleCode/components/LeftMenu'
+import ApprovalFlow from './components/Flow'
+type iState = {
+  activeKey: string | null
+  process: API.ApprovalProcess[]
+  subjectName?: string
+}
 const Step: React.FC = () => {
-  return <div>步骤设置</div>
+  const [drawer, DrawerDOM] = useDrawer()
+  const actionRef = useRef<ActionType>()
+  const [state, setState] = useState<iState>({
+    activeKey: null,
+    process: []
+  })
+
+  const params = useMemo(() => {
+    if (!state.activeKey) return {}
+    const [subjectID, businessType] = state.activeKey.split('_')
+    return { subjectID, businessType }
+  }, [state.activeKey])
+
+  const handleMenuChange = (key: string) => {
+    console.log(key)
+
+    setState({ ...state, activeKey: key })
+    actionRef.current?.reload()
+    // const [subjectID, businessType] = key.split('_')
+    //
+  }
+  const columns: ProColumns = [
+    {
+      dataIndex: 'name',
+      title: '步骤名称',
+      width: '50%'
+    },
+    {
+      dataIndex: 'executor',
+      title: '执行者',
+      width: '20%'
+    },
+    {
+      dataIndex: 'matter',
+      title: '事项名称',
+      width: '30%'
+    }
+  ]
+
+  const handleBtnClick = () => {
+    console.log(transformElements(mockNodes).filter(isNode))
+
+    drawer.open({
+      title: state.subjectName,
+      children: <ApprovalFlow defaultElements={transformElements(mockNodes)} />
+    })
+  }
+  return (
+    <PageContainer title={false}>
+      <div className="h-full w-full flex flex-row">
+        <LeftMenu title="业务主体" onChange={handleMenuChange} />
+        <div className="w-6/7 ml-8 bg-white rounded-20px">
+          <ProTable
+            manualRequest
+            actionRef={actionRef}
+            params={params}
+            columns={columns}
+            search={false}
+            toolbar={{
+              actions: [
+                <Button key="step-config" type="primary" onClick={handleBtnClick}>
+                  步骤配置
+                </Button>
+              ]
+            }}
+            request={async params => {
+              const {
+                code,
+                data: { subjectName, process = [], processList = [] }
+              } = await queryApprovalDetail(params)
+              setState({ ...state, process, subjectName })
+              return {
+                total: processList.length,
+                data: processList,
+                success: code === consts.RET_CODE.SUCCESS
+              }
+            }}
+          />
+        </div>
+      </div>
+      {DrawerDOM}
+    </PageContainer>
+  )
 }
 
 export default Step

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

@@ -105,7 +105,7 @@ export async function setProjectPersonor(params: { ID: string; createdID: string
 }
 
 /** 审批流程-详情 */
-export async function queryApprovalDetail(params: { ID: string }) {
+export async function queryApprovalDetail(params: { subjectID: string; businessType: string }) {
   return request('/approval/detail', {
     params
   })