Sfoglia il codice sorgente

feat: 步骤设置关联事项与事项权限设置

lanjianrong 2 anni fa
parent
commit
bbc9c0fde6

+ 0 - 2
src/components/Drawer/index.less

@@ -10,6 +10,4 @@
   :global(.ant-drawer-body) {
     padding: 0 24px;
   }
-  :global(.ant-drawer-close) {
-  }
 }

+ 1 - 0
src/components/Drawer/index.tsx

@@ -124,6 +124,7 @@ const DrawerHoc = memo(
         className={mergedClassName}
         title={mergedTitle}
         mask={mergedMask}
+        closable={false}
         push={push}
         width={mergedWidth}
         style={mergedStyle}

+ 2 - 1
src/pages/Business/Matter/index.tsx

@@ -35,7 +35,8 @@ const Matter: React.FC = () => {
   const openDrawer = () => {
     drawer.open({
       title: '组件配置',
-      closeIcon: (
+      closeIcon: null,
+      extra: (
         <Button
           onClick={() => {
             modal.open({

+ 1 - 1
src/pages/Business/Process/components/LeftMenu/index.tsx

@@ -11,7 +11,7 @@ type LeftMenuProps = {
 const LeftMenu: React.FC<LeftMenuProps> = ({ title = '流程用户设置', options, onChange, value }) => {
   return (
     <div className="w-216px rounded-20px" style={{ height: 'calc(100vh - 122px)', background: '#ffffff' }}>
-      <div className="p-5 text-16px text-opacity-85 menu-title">{title}</div>
+      <div className="p-5 text-16px text-opacity-85">{title}</div>
       <Menu
         defaultSelectedKeys={[value]}
         onSelect={({ key }) => onChange(key)}

+ 49 - 2
src/pages/Business/Step/components/Flow/components/Drawer/Auditor/MatterPermission.tsx

@@ -1,5 +1,52 @@
-const MatterPermission = () => {
-  return <div>步骤设置内容</div>
+import { Card, Checkbox } from 'antd'
+
+export enum MatterPerm {
+  FORM = 'formPermission',
+  COST = 'costPermission',
+  PROFILE = 'profilePermission'
+}
+
+const matterPermMap = {
+  [MatterPerm.FORM]: '表单',
+  [MatterPerm.COST]: '造价文件',
+  [MatterPerm.PROFILE]: '资料清单'
+}
+
+type MatterPermissionProps = {
+  defaultValue: {
+    matterID: string
+    formPermission: string[]
+    costPermission: string[]
+    profilePermission: string[]
+  }[]
+  toggleMatter: (id: string, perms?: Record<string, string[]>) => void
+  matterList: API.MatterItem[]
+}
+
+const MatterPermission: React.FC<MatterPermissionProps> = ({ defaultValue, toggleMatter, matterList }) => {
+  return defaultValue.map(matter => (
+    <Card
+      key={matter.matterID}
+      size="small"
+      className="mb-2"
+      title={matterList.find(item => item.ID === matter.matterID)?.name}>
+      {[MatterPerm.COST, MatterPerm.FORM, MatterPerm.PROFILE].map(item => (
+        <div key={item} className="flex justify-between items-center my-1">
+          <div className="flex-1">{matterPermMap[item]}</div>
+          <div className="w-40">
+            <Checkbox.Group
+              defaultValue={matter[item]}
+              options={[
+                { label: '查看', value: 'view' },
+                { label: '编辑', value: 'edit' }
+              ]}
+              onChange={checkedValue => toggleMatter(matter.matterID, { [item]: checkedValue })}
+            />
+          </div>
+        </div>
+      ))}
+    </Card>
+  ))
 }
 
 export default MatterPermission

+ 36 - 2
src/pages/Business/Step/components/Flow/components/Drawer/Auditor/RelatedMatter.tsx

@@ -1,5 +1,39 @@
-const RelatedMatter = () => {
-  return <div>步骤设置内容</div>
+import { Checkbox, Table } from 'antd'
+import { ColumnType } from 'antd/lib/table'
+
+type RelatedMatterpProps = {
+  matterList: any[]
+  toggleMatter: (id: string) => void
+  defaultValue: string[]
+}
+const RelatedMatter: React.FC<RelatedMatterpProps> = ({ defaultValue, matterList, toggleMatter }) => {
+  const columns: ColumnType<API.MatterItem>[] = [
+    {
+      dataIndex: 'name',
+      title: '事项名称',
+      width: '80%',
+      onHeaderCell: () => ({ style: { textAlign: 'center' } })
+    },
+    {
+      dataIndex: 'opreate',
+      title: '执行中',
+      align: 'center',
+      render: (_, record) => {
+        return (
+          <Checkbox
+            defaultChecked={defaultValue.includes(record.ID)}
+            onChange={() => toggleMatter(record.ID)}>
+            查看
+          </Checkbox>
+        )
+      }
+    }
+  ]
+  return (
+    matterList && (
+      <Table rowKey="ID" size="small" columns={columns} dataSource={matterList} bordered pagination={false} />
+    )
+  )
 }
 
 export default RelatedMatter

+ 5 - 4
src/pages/Business/Step/components/Flow/components/Drawer/Auditor/StepSetting.tsx

@@ -1,7 +1,7 @@
 import { QuestionCircleFilled } from '@ant-design/icons'
 import ProForm, { ProFormList, ProFormSelect, ProFormText } from '@ant-design/pro-form'
 import { FormInstance, FormListOperation } from 'antd'
-import { useRef } from 'react'
+import { ReactNode, useRef } from 'react'
 
 type StepSettingProps = {
   dataID: string
@@ -9,14 +9,15 @@ type StepSettingProps = {
   disabled: boolean
   stepOptions: { label: string; value: string }[]
   executorOptions: { label: string; value: string }[]
+  extra?: ReactNode
 }
-const StepSetting: React.FC<StepSettingProps> = ({ form, disabled, stepOptions, executorOptions }) => {
+const StepSetting: React.FC<StepSettingProps> = ({ form, disabled, stepOptions, executorOptions, extra }) => {
   const actionRef = useRef<FormListOperation>()
 
   return (
     <ProForm submitter={false} layout="horizontal" labelCol={{ span: 4 }} labelAlign="left" form={form}>
       <div className="divide-y divide-dashed">
-        <div>
+        <div className="pb-4">
           <div className="font-medium mb-3">
             <span>本步骤配置</span>
             <QuestionCircleFilled style={{ color: '#FEA100' }} className="ml-1" />
@@ -31,9 +32,9 @@ const StepSetting: React.FC<StepSettingProps> = ({ form, disabled, stepOptions,
             name="executorID"
             label="步骤执行者"
             placeholder="请选择"
+            help={extra ? <div className="mt-1">{extra}</div> : undefined}
             disabled={disabled}
             options={executorOptions}
-            rules={[{ required: true, message: '请选择步骤执行者' }]}
           />
         </div>
         <div className="">

+ 98 - 21
src/pages/Business/Step/components/Flow/components/Drawer/Auditor/index.tsx

@@ -1,11 +1,11 @@
 import StepSetting from './StepSetting'
 import RelatedMatter from './RelatedMatter'
 import MatterPermission from './MatterPermission'
-import { Button, Form, Tabs } from 'antd'
-import { useContext, useEffect } from 'react'
+import { Button, Form, Tabs, Tag } from 'antd'
+import { useContext, useEffect, useState } from 'react'
 import { FlowContext } from '../../../context'
 import { Edge, isNode } from 'react-flow-renderer'
-import { updateNodeData } from '../../Edge/utils'
+import { findNodeById, updateNodeData } from '../../Edge/utils'
 
 type AuditorProps = {
   nodeId: string
@@ -19,8 +19,9 @@ type AuditorProps = {
 const Auditor: React.FC<AuditorProps> = ({ nodeId, nodeData: { name, participantInfo } }) => {
   const [form] = Form.useForm()
 
+  const [stepMatters, setStepMatters] = useState(participantInfo?.stepMatters || [])
   const {
-    flowStore: { executorList, process, drawer, elements },
+    flowStore: { dataID, executorList, process, drawer, elements, matterList },
     dispatch
   } = useContext(FlowContext)
 
@@ -28,7 +29,7 @@ const Auditor: React.FC<AuditorProps> = ({ nodeId, nodeData: { name, participant
     const initalValues = {
       name,
       executorID: participantInfo?.executor?.ID,
-      dynamicSteps: participantInfo?.executor?.dynamicSteps
+      dynamicSteps: participantInfo?.dynamicSteps
     }
     form.setFieldsValue(initalValues)
   }, [])
@@ -40,6 +41,43 @@ const Auditor: React.FC<AuditorProps> = ({ nodeId, nodeData: { name, participant
     .filter((item, idx) => item.data?.name && item.id !== nodeId && idx > currStepIdx)
     .map(item => ({ label: item.data.name, value: item.id }))
 
+  const renderStepSettingExtraNode = () => {
+    if (participantInfo?.dynamicID) {
+      const step = elements.find(item => item.id === participantInfo.dynamicID)
+      return (
+        step && (
+          <div>
+            当前执行者由
+            <Tag style={{ margin: '0 8px' }}>{step.data?.name}</Tag>
+            配置
+          </div>
+        )
+      )
+    }
+  }
+
+  const toggleMatter = (matterID: string, perms?: Record<string, string[]>) => {
+    if (stepMatters.find(item => item.matterID === matterID)) {
+      // perms存在说明是更新事项权限
+      if (perms) {
+        setStepMatters(
+          stepMatters.map(item => {
+            if (item.matterID === matterID) {
+              return { ...item, ...perms }
+            }
+            return item
+          })
+        )
+      } else {
+        setStepMatters(stepMatters.filter(item => item.matterID !== matterID))
+      }
+    } else {
+      setStepMatters([...stepMatters, { matterID, ...perms }])
+    }
+  }
+
+  console.log(stepMatters)
+
   const items = [
     {
       label: '步骤设置',
@@ -47,37 +85,76 @@ const Auditor: React.FC<AuditorProps> = ({ nodeId, nodeData: { name, participant
       children: (
         <StepSetting
           executorOptions={executorList.map(item => ({ label: item.name, value: item.ID }))}
-          disabled={participantInfo?.executor?.ID === '业务发起人'}
+          disabled={participantInfo?.dynamicID || participantInfo?.executor?.ID === '业务发起人'}
           form={form}
+          extra={renderStepSettingExtraNode()}
           stepOptions={stepOptions}
         />
       )
     },
-    { label: '关联事项', key: 'relatedMatter', children: <RelatedMatter /> },
-    { label: '事项权限', key: 'matterPermission', children: <MatterPermission /> }
+    {
+      label: '关联事项',
+      key: 'relatedMatter',
+      children: (
+        <RelatedMatter
+          defaultValue={stepMatters.map(item => item.matterID)}
+          dataID={dataID}
+          matterList={matterList}
+          toggleMatter={toggleMatter}
+        />
+      )
+    },
+    {
+      label: '事项权限',
+      key: 'matterPermission',
+      children: (
+        <MatterPermission defaultValue={stepMatters} matterList={matterList} toggleMatter={toggleMatter} />
+      )
+    }
   ]
 
   const onConfirm = () => {
     form
       .validateFields()
       .then(values => {
-        const needUpdatedNode = {
-          id: nodeId,
-          data: {
-            name: values.name,
-            participantInfo: {
-              approvalWay: 'account',
-              executor: {
-                ID: values.executorID,
-                configure: ['return']
-              },
-              dynamicSteps: values.dynamicSteps || []
+        const needUpdatedNodes = [
+          {
+            id: nodeId,
+            data: {
+              name: values.name,
+              participantInfo: {
+                approvalWay: 'account',
+                executor: {
+                  ID: values.executorID,
+                  configure: ['return']
+                },
+                dynamicSteps: values.dynamicSteps || [],
+                stepMatters
+              }
             }
           }
-        }
+        ]
+        values.dynamicSteps?.forEach((dynamicStep: { stepID: string; executorID: string }) => {
+          const node = findNodeById(process, dynamicStep.stepID)
+          node &&
+            needUpdatedNodes.push({
+              ...node,
+              data: {
+                ...node.data,
+                participantInfo: {
+                  ...node.data?.participantInfo,
+                  dynamicID: nodeId,
+                  executor: { ...node.data?.participantInfo?.executor, ID: dynamicStep.executorID }
+                }
+              }
+            })
+        })
+
+        console.log(needUpdatedNodes)
+
         dispatch({
           type: 'set_flow_process',
-          payload: updateNodeData(process, needUpdatedNode)
+          payload: updateNodeData(process, needUpdatedNodes)
         })
         drawer.close()
       })

+ 42 - 45
src/pages/Business/Step/components/Flow/components/Edge/BaseEdge.tsx

@@ -115,55 +115,52 @@ export default ({ id, source, target, path, centerX, centerY, style }: BaseEdgeP
           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>
+          y={centerY - 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={() => addAuditorOrCondition('auditor')}
+                      icon={<SolutionOutlined />}>
+                      审批人
+                    </Button>
+                  </li>
+                  {!showConditionItem && (
+                    <li className="mt-2 condition">
                       <Button
                         type="dashed"
-                        onClick={() => addAuditorOrCondition('auditor')}
-                        icon={<SolutionOutlined />}>
-                        审批人
+                        onClick={() => addAuditorOrCondition('condition')}
+                        icon={<BranchesOutlined />}>
+                        条件分支
                       </Button>
                     </li>
-                    {!showConditionItem && (
-                      <li className="mt-2 condition">
-                        <Button
-                          type="dashed"
-                          onClick={() => addAuditorOrCondition('condition')}
-                          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>
+                  )}
+                </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>
         </foreignObject>
       )}
     </>

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

@@ -3,7 +3,10 @@
   stroke-width: 1;
 }
 .addIcon {
+  height: 100%;
+  width: 100%;
   display: flex;
+  background: transparent;
   align-items: center;
   justify-content: center;
   :global(.ant-btn) {
@@ -15,11 +18,3 @@
     // }
   }
 }
-.edgebuttonForeignobject {
-  & > body {
-    background: transparent;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-  }
-}

+ 9 - 5
src/pages/Business/Step/components/Flow/components/Edge/utils.ts

@@ -140,15 +140,19 @@ export function findNodeById(nodes: FlowTreeNode[], id: string): FlowTreeNode {
 /**
  * 更新节点数据
  * @param nodes 原始节点数据
- * @param node 要更新的数据
+ * @param updatedNodes 要更新的数据
  */
-export function updateNodeData(nodes: FlowTreeNode[], updatedNode: FlowTreeNode) {
+export function updateNodeData(nodes: FlowTreeNode[], updatedNodes: FlowTreeNode[]) {
+  const nodesMap = updatedNodes.reduce((prev, curr) => {
+    prev[curr.id] = curr
+    return prev
+  }, {})
   return nodes.map(item => {
-    if (item.id === updatedNode.id) {
-      item.data = updatedNode.data
+    if (nodesMap[item.id]) {
+      item.data = { ...item.data, ...nodesMap[item.id].data }
     }
     if (item.children?.length) {
-      item.children = updateNodeData(item.children, updatedNode)
+      item.children = updateNodeData(item.children, updatedNodes)
     }
     return item
   })

+ 7 - 3
src/pages/Business/Step/components/Flow/context/index.tsx

@@ -10,14 +10,17 @@ export type FlowContextState = {
   dataID: string
   process: FlowTreeNode[]
   elements: Elements
-  executorList: API.ExecutorItem[]
   flowInstance?: OnLoadParams
   drawer: DrawerAction
+  executorList: API.ExecutorItem[]
+  matterList: any[]
 }
 type FlowContextProviderProps = {
   children: React.ReactNode
   initialProcess: Elements
   dataID: string
+  executorList: API.ExecutorItem[]
+  matterList: any[]
 }
 
 export type DispatchAction = Dispatch<{
@@ -28,12 +31,13 @@ export type DispatchAction = Dispatch<{
 const FlowContext = createContext<{ flowStore: FlowContextState; dispatch: DispatchAction }>({})
 
 const FlowContextProvider: React.FC<FlowContextProviderProps> = props => {
-  const { children, initialProcess = [], dataID, executorList = [] } = props
+  const { children, initialProcess = [], dataID, executorList = [], matterList = [] } = props
   const [flowStore, dispatch] = useReducer(reducer, {
     process: initialProcess,
     elements: transformElements(initialProcess),
     dataID,
-    executorList
+    executorList,
+    matterList
   })
   return <FlowContext.Provider value={{ flowStore, dispatch }}>{children}</FlowContext.Provider>
 }

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

@@ -42,7 +42,7 @@
     justify-content: end;
     // margin: 12px;
     box-shadow: none;
-    & .react-flow-controls-zoomout {
+    & .react-flow__controls-zoomout {
       margin-left: 8px;
     }
   }

+ 7 - 2
src/pages/Business/Step/components/Flow/index.tsx

@@ -9,12 +9,17 @@ type ApprovalFlowProps<T> = {
   defaultValue?: FlowTreeNode<T>[]
   dataID: string
   executorList: API.ExecutorItem[]
+  matterList: any[]
 }
 
-function ApprovalFlow<T>({ defaultValue, dataID, executorList }: ApprovalFlowProps<T>) {
+function ApprovalFlow<T>({ defaultValue, dataID, executorList, matterList }: ApprovalFlowProps<T>) {
   return (
     <div className="flow-container h-full w-full">
-      <FlowContextProvider initialProcess={defaultValue} dataID={dataID} executorList={executorList}>
+      <FlowContextProvider
+        initialProcess={defaultValue}
+        dataID={dataID}
+        executorList={executorList}
+        matterList={matterList}>
         <ReactFlowProvider>
           {/** 顶部工具栏 */}
           <ToolBar />

+ 42 - 21
src/pages/Business/Step/index.tsx

@@ -1,20 +1,29 @@
 import useDrawer from '@/components/Drawer'
-import { queryExecutorList } from '@/services/api/business'
+import { queryMatterList } from '@/services/api/business'
 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 { connect } from '@umijs/max'
 import { Button } from 'antd'
-import { useMemo, useRef, useState } from 'react'
+import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
+import { BusinessModelState } from '../model'
 import LeftMenu from '../RuleCode/components/LeftMenu'
 import ApprovalFlow from './components/Flow'
 import { transformToFlowNodes } from './components/Flow/shared/transformer'
+
+type StepProps = {
+  processMap: {
+    [key: string]: API.ExecutorItem[]
+  }
+  dispatch: Dispatch
+}
 type iState = {
   activeKey: string | null
   process: API.ApprovalProcess[]
   subjectName?: string
 }
-const Step: React.FC = () => {
+const Step: React.FC<StepProps> = ({ processMap, dispatch }) => {
   const [drawer, DrawerDOM] = useDrawer()
   const actionRef = useRef<ActionType>()
   const [state, setState] = useState<iState>({
@@ -22,6 +31,15 @@ const Step: React.FC = () => {
     process: []
   })
 
+  useEffect(() => {
+    if (state.activeKey) {
+      const [subjectID, businessType] = state.activeKey.split('_')
+      dispatch({
+        type: 'business/queryExecutor',
+        payload: { subjectID, businessType, pageSize: 214000 }
+      })
+    }
+  }, [state.activeKey])
   const params = useMemo(() => {
     if (!state.activeKey) return {}
     const [subjectID, businessType] = state.activeKey.split('_')
@@ -55,23 +73,24 @@ const Step: React.FC = () => {
   ]
 
   const handleBtnClick = async () => {
-    const [subjectID, businessType] = state.activeKey?.split('_')
-    const {
-      code = -1,
-      data: { items }
-    } = await queryExecutorList({ subjectID, businessType, pageSize: 214000 })
-    if (code === consts.RET_CODE.SUCCESS) {
-      drawer.open({
-        title: state.subjectName,
-        children: (
-          <ApprovalFlow
-            executorList={items}
-            dataID={state.activeKey}
-            defaultElements={transformToFlowNodes(state.process)}
-          />
-        )
-      })
-    }
+    const [subjectID, businessType] = state.activeKey.split('_')
+    queryMatterList({ subjectID, businessType, pageSize: 214000, matterType: 'matter' }).then(
+      ({ code = -1, data: { items: matters = [] } = {} }) => {
+        if (code === consts.RET_CODE.SUCCESS) {
+          drawer.open({
+            title: state.subjectName,
+            children: (
+              <ApprovalFlow
+                executorList={processMap[state.activeKey]}
+                matterList={matters}
+                dataID={state.activeKey}
+                defaultElements={transformToFlowNodes(state.process)}
+              />
+            )
+          })
+        }
+      }
+    )
   }
   return (
     <PageContainer title={false}>
@@ -111,4 +130,6 @@ const Step: React.FC = () => {
   )
 }
 
-export default Step
+export default connect(({ business }: { business: BusinessModelState }) => ({
+  processMap: business.processMap
+}))(Step)

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

@@ -445,6 +445,14 @@ declare namespace API {
   type ParticipantInfo = {
     approvalWay?: string
     executor?: Executor
+    dynamicID?: string
+    dynamicSteps?: { stepID: string; executorID: string }[]
+    stepMatters?: {
+      matterID: string
+      formPermission: string[]
+      costPermission: string[]
+      profilePermission: string[]
+    }[]
   }
   type ConditionInfo = {
     ID?: string

+ 3 - 2
unocss.config.ts

@@ -1,4 +1,4 @@
-import { defineConfig, presetWind } from 'unocss'
+import { defineConfig, presetWind, transformerDirectives, transformerVariantGroup } from 'unocss'
 import proSettings from './config/defaultSettings'
 export function createConfig({ dev = true } = {}) {
   return defineConfig({
@@ -18,7 +18,8 @@ export function createConfig({ dev = true } = {}) {
       boxShadow: {
         card: '0 0 13px 0 rgba(74, 53, 107, 0.08)'
       }
-    }
+    },
+    transformers: [transformerDirectives(), transformerVariantGroup()]
   })
 }