瀏覽代碼

feat: 111

lanjianrong 3 年之前
父節點
當前提交
601800c2bc

+ 145 - 0
src/components/Flow/src/components/Drawer/index.tsx

@@ -0,0 +1,145 @@
+import { useRef, useState, useMemo, useContext } from 'react'
+import { Button, Drawer, Radio, Select, Spin, Tag } from 'antd'
+import { Actions, FlowContext } from '../../context'
+import debounce from 'lodash/debounce'
+import type { RadioChangeEvent } from 'antd'
+import { queryAcountList } from '@/services/api/institution'
+import consts from '@/utils/consts'
+const debounceTimeout = 800
+
+export enum ApprovalType {
+  TARGET = '1', // 指定成员
+  SUPERIOR = '2', // 上级
+  ROLE = '3', // 角色
+  INITIATOR = '4', // 发起人自选
+  MULTISTAGE = '5' // 连续多级上级
+}
+
+const FlowDrawer = () => {
+  const { flowState, dispatch } = useContext(FlowContext)
+  const { drawerConfig } = flowState
+
+  const fetchRef = useRef(0)
+  const [state, setState] = useState({
+    approvalType: ApprovalType.TARGET,
+    fetching: false,
+    options: [],
+    staffOptions: []
+  })
+
+  const handleCancel = () => dispatch({ type: Actions.CLOSE_MODAL })
+
+  const plainOptions = [
+    { label: '指定成员', value: ApprovalType.TARGET },
+    { label: '上级', value: ApprovalType.SUPERIOR },
+    { label: '角色', value: ApprovalType.ROLE },
+    { label: '发起人自选', value: ApprovalType.INITIATOR },
+    { label: '连续多级上级', value: ApprovalType.MULTISTAGE }
+  ]
+
+  const approvalTypeChange = (e: RadioChangeEvent) => {
+    const { target } = e
+    if (target?.value) {
+      setState({ ...state, approvalType: target.value })
+    }
+  }
+  const fetchOptions = async params => {
+    const { code = -1, data = {} } = await queryAcountList({
+      ...params,
+      current: 1,
+      pageSize: 100,
+      dataID: 'e01b8695-c43b-4d2d-8169-a021aa6b69ae'
+    })
+    if (code === consts.RET_CODE.SUCCESS) {
+      return data.items.map(item => ({ label: item.name, value: item.ID }))
+    }
+    return []
+  }
+
+  const debounceFetcher = useMemo(() => {
+    const loadOptions = params => {
+      fetchRef.current += 1
+      const fetchId = fetchRef.current
+      setState({ ...state, options: [], fetching: true })
+
+      fetchOptions(params).then(newOptions => {
+        if (fetchId !== fetchRef.current) {
+          // for fetch callback order
+          return
+        }
+
+        setState({ ...state, options: newOptions, fetching: false })
+      })
+    }
+
+    return debounce(loadOptions, debounceTimeout)
+  }, [fetchOptions, debounceTimeout])
+
+  const staffIds = state.staffOptions.map(i => i.value)
+  const triggerChange = (val, option) => {
+    if (!staffIds.includes(val)) {
+      setState({ ...state, staffOptions: [...state.staffOptions, option] })
+    }
+  }
+
+  const handleOnOk = () => {
+    const payload = {
+      id: drawerConfig.nodeId,
+      node: {
+        approvalType: state.approvalType,
+        staffOptions: state.staffOptions
+      }
+    }
+    dispatch({
+      type: Actions.SET_FLOW_NODE,
+      payload
+    })
+    handleCancel()
+  }
+
+  return (
+    <Drawer
+      visible={drawerConfig.visible}
+      onClose={handleCancel}
+      footer={
+        <div className="flex justify-end">
+          <Button onClick={handleCancel}>取消</Button>
+          <Button className="ml-8px" type="primary" onClick={handleOnOk}>
+            确认
+          </Button>
+        </div>
+      }
+      title={<span className="font-medium">设置审批人</span>}
+      width="30%">
+      <div>
+        <Radio.Group
+          options={plainOptions}
+          onChange={approvalTypeChange}
+          value={state.approvalType}
+        />
+        <div className="mt-40px flex flex-col">
+          <div className="text-sm mb-4">
+            <span className="font-medium mr-1 text-14px">添加员工</span>
+            <span className="text-hex-000000 text-opacity-45">不得超过20人</span>
+          </div>
+          <Select
+            showSearch
+            value={null}
+            onChange={triggerChange}
+            filterOption={false}
+            onSearch={v => debounceFetcher({ search: v })}
+            notFoundContent={state.fetching ? <Spin size="small" /> : null}
+            options={state.options?.filter(item => !staffIds.includes(item.value))}
+          />
+          <div className="mt-4">
+            {state.staffOptions.map(item => (
+              <Tag key={item.value}>{item.label}</Tag>
+            ))}
+          </div>
+        </div>
+      </div>
+    </Drawer>
+  )
+}
+
+export default FlowDrawer

+ 7 - 5
src/components/Flow/src/components/Edge/index.tsx

@@ -9,7 +9,7 @@ import styles from './index.less'
 import 'antd/lib/button/style/css'
 const foreignObjectSize = 50
 
-const renderMenu = ({ dispatch, elements, togglePopver, id, flowInstance }) => {
+const renderMenu = ({ dispatch, elements, togglePopver, id, flowInstance, sort }) => {
   const setElements = els => {
     dispatch({
       type: Actions.SET_ELEMENTS,
@@ -48,11 +48,13 @@ const renderMenu = ({ dispatch, elements, togglePopver, id, flowInstance }) => {
       type: Actions.SET_FLOW_NODE,
       payload: {
         id: uid,
-        node: {}
+        node: null
       }
     })
-    flowInstance?.fitView()
     togglePopver(false)
+    setTimeout(() => {
+      flowInstance?.fitView()
+    }, 80)
   }
 
   return (
@@ -84,8 +86,8 @@ export function CommonEdge(props) {
   } = props
 
   const [visible, setVisible] = useState(false)
-  const { state, dispatch } = useContext(FlowContext)
-  const { elements, flowInstance } = state
+  const { flowState, dispatch } = useContext(FlowContext)
+  const { elements, flowInstance } = flowState
   const nodes = useStoreState(store => store.nodes)
   const markerEnd = getMarkerEnd(arrowHeadType, markerEndId)
 

+ 23 - 16
src/components/Flow/src/components/Graph/FloatingEdge.tsx

@@ -1,20 +1,21 @@
-import { FC, useMemo, CSSProperties } from 'react';
-import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer';
-
-import { getEdgeParams } from './utils';
+import { getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer'
+import { getEdgeParams } from './utils'
+import { useMemo } from 'react'
+import type { FC, CSSProperties } from 'react'
+import type { EdgeProps } from 'react-flow-renderer'
 
 const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, markerEndId, style }) => {
-  const nodes = useStoreState((state) => state.nodes);
-  const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
+  const nodes = useStoreState(state => state.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]);
+  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;
+    return null
   }
 
-  const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
+  const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
 
   const d = getBezierPath({
     sourceX: sx,
@@ -22,14 +23,20 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, marker
     sourcePosition: sourcePos,
     targetPosition: targetPos,
     targetX: tx,
-    targetY: ty,
-  });
+    targetY: ty
+  })
 
   return (
     <g className="react-flow__connection">
-      <path id={id} className="react-flow__edge-path" d={d} markerEnd={markerEnd} style={style as CSSProperties} />
+      <path
+        id={id}
+        className="react-flow__edge-path"
+        d={d}
+        markerEnd={markerEnd}
+        style={style as CSSProperties}
+      />
     </g>
-  );
-};
+  )
+}
 
-export default FloatingEdge;
+export default FloatingEdge

+ 2 - 2
src/components/Flow/src/components/Graph/index.tsx

@@ -17,8 +17,8 @@ const edgeTypes: EdgeTypesType = {
 }
 
 export default function FlowGroph() {
-  const { state, dispatch } = useContext(FlowContext)
-  const { elements } = state
+  const { flowState, dispatch } = useContext(FlowContext)
+  const { elements } = flowState
 
   const defaultOptions = {
     nodesDraggable: false, // 不可拖拽

+ 22 - 8
src/components/Flow/src/components/Node/index.tsx

@@ -26,11 +26,10 @@ export const OutputNode = () => {
 }
 
 export const CommonNode = props => {
-  // console.log(props)
   const { id, data } = props
 
-  const { state, dispatch } = useContext(FlowContext)
-  const { flowInstance } = state
+  const { flowState, dispatch } = useContext(FlowContext)
+  const { flowInstance, flowData } = flowState
   const removeNode = () => {
     dispatch({
       type: Actions.REMOVE_FLOW_NODE,
@@ -39,8 +38,21 @@ export const CommonNode = props => {
         data
       }
     })
-    flowInstance?.fitView()
+    setTimeout(() => {
+      flowInstance?.fitView()
+    }, 80)
   }
+  const openDrawer = () => {
+    dispatch({
+      type: Actions.OPEN_MODAL,
+      payload: {
+        id
+      }
+    })
+  }
+
+  const auditor = flowData.get(id)
+
   return (
     <>
       <Handle type="target" position="top" />
@@ -61,16 +73,18 @@ export const CommonNode = props => {
         </div>
       </div> */}
       <div className="shadow-card cursor-default p-1 bg-hex-1890ff rounded-lg node_content">
-        <div className="relative text-light-500 leading-7 h-7 text-12px ml-1 flex justify-between items-center">
-          <div>审批人</div>
+        <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 cursor-pointer">
-          <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 cursor-pointer"
+          onClick={openDrawer}>
+          <div>{auditor ? auditor.staffOptions?.[0]?.label : '请选择审批人'}</div>
           <div>
             <RightOutlined />
           </div>

+ 20 - 7
src/components/Flow/src/components/Toolbar/index.tsx

@@ -1,16 +1,29 @@
+import { CloudUploadOutlined } from '@ant-design/icons'
+import { useContext } from 'react'
 import { Button } from 'antd'
+import { FlowContext } from '../../context'
+import { addApprovalFlow } from '@/services/api/project'
 
 export default function ToolBar() {
-  // 保存
-  const handleSave = () => {}
+  const { flowState } = useContext(FlowContext)
+  const handleSave = async () => {
+    // const formData = {
+    //   nodeData: { ...flowState.flowData }
+    // }
 
-  // 重置节点
-  const handleRest = () => {}
+    const { code = -1, msg } = await addApprovalFlow({
+      ID: 'a905f20b-ce73-48c7-9173-8520ca9ebe66',
+      flowProcess: flowState.elements,
+      flowProcessData: Object.fromEntries(flowState.flowData.entries())
+    })
+    console.log(msg)
+  }
 
   return (
-    <div className="py-2 px-4 text-right children:mx-2">
-      <Button onClick={handleRest}>重置</Button>
-      <Button onClick={handleSave}>保存</Button>
+    <div className="absolute l-0 t-0 z-10">
+      <Button onClick={handleSave} icon={<CloudUploadOutlined />} type="primary">
+        发布流程
+      </Button>
     </div>
   )
 }

+ 3 - 3
src/components/Flow/src/context/index.tsx

@@ -43,7 +43,7 @@ const initialState: InitialState = {
     }
   ],
   flowData: new Map(),
-  modalConfig: {
+  drawerConfig: {
     visible: false,
     nodeType: '',
     nodeID: ''
@@ -53,8 +53,8 @@ const initialState: InitialState = {
 
 const FlowContextProvider = props => {
   const { children } = props
-  const [state, dispatch] = useReducer(reducer, initialState)
-  return <FlowContext.Provider value={{ state, dispatch }}>{children}</FlowContext.Provider>
+  const [flowState, dispatch] = useReducer(reducer, initialState)
+  return <FlowContext.Provider value={{ flowState, dispatch }}>{children}</FlowContext.Provider>
 }
 
 export { FlowContext, FlowContextProvider, Actions }

+ 10 - 9
src/components/Flow/src/context/reducer.ts

@@ -1,5 +1,5 @@
 // context/reducer.js
-import { removeElements } from 'react-flow-renderer'
+// import { removeElements } from 'react-flow-renderer'
 
 import type { Elements } from 'react-flow-renderer'
 import { generateElements, genreateElementEnum } from '../utils'
@@ -17,11 +17,11 @@ const setElements = (state, elements: Elements) => ({
   elements: Array.isArray(elements) ? elements : []
 })
 
-const setFlowNode = (state, node) => {
-  if (!node || !node.id) return state
-  const nodeId = node.id
+const setFlowNode = (state, { id, node }) => {
+  if (!id) return state
   const res = { ...state }
-  res.flowData.set(nodeId, node)
+
+  res.flowData.set(id, node)
   return res
 }
 
@@ -43,21 +43,22 @@ const removeFlowNode = (state, node) => {
   return res
 }
 
-const openModal = (state, node) =>
-  node?.id
+const openModal = (state, node) => {
+  return node?.id
     ? {
         ...state,
-        modalConfig: {
+        drawerConfig: {
           visible: true,
           nodeType: node.type,
           nodeId: node.id
         }
       }
     : state
+}
 
 const closeModal = state => ({
   ...state,
-  modalConfig: {
+  drawerConfig: {
     visible: false,
     nodeType: '',
     nodeId: ''

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

@@ -6,6 +6,7 @@ import './index.less'
 import { FlowContext, FlowContextProvider } from './context'
 import ToolBar from './components/Toolbar'
 import FlowGroph from './components/Graph'
+import FlowDrawer from './components/Drawer'
 
 // type ApprovalFlowProps<T = any> = {
 //   elements: Elements<T>
@@ -33,8 +34,9 @@ const ApprovalFlow: FC<ApprovalFlowProps> = () => {
       <FlowContextProvider>
         <ReactFlowProvider>
           {/** 顶部工具栏 */}
-          {/* <ToolBar /> */}
+          <ToolBar />
           <FlowGroph />
+          <FlowDrawer />
         </ReactFlowProvider>
       </FlowContextProvider>
     </div>

+ 3 - 2
src/pages/Institutions/Company/Detail/components/Organization.tsx

@@ -231,8 +231,9 @@ const Organization: React.FC<OrganizationProps> = ({ dataID, structureType }) =>
           expandedRowKeys: state.expandTreeIds,
           onExpand: (expanded, record) => {
             if (expanded) {
-              if (state.expandTreeIds.includes(record.ID)) return
-              setState({ ...state, expandTreeIds: [...state.expandTreeIds, record.ID] })
+              if (!state.expandTreeIds.includes(record.ID)) {
+                return setState({ ...state, expandTreeIds: [...state.expandTreeIds, record.ID] })
+              }
             } else {
               setState({
                 ...state,

+ 8 - 0
src/services/api/project.ts

@@ -39,3 +39,11 @@ export async function addProject(params: API.ProjectAddParams) {
     data: params
   })
 }
+
+/** 更新审批流程图 */
+export async function addApprovalFlow(params) {
+  return request('/approval/update/process', {
+    method: 'POST',
+    data: params
+  })
+}