尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

第 5 章:审批工作流

第 5 章:审批工作流 本章学习目标理解多级审批模型的设计思路掌握审批流状态机的实现学会设计审批操作界面同意/驳回 审批意见理解操作审计日志的设计与实现实现审批记录时间线5.1 多级审批模型设计审批工作流是企业系统中常见的功能。一个任务单可能需要经过一级、二级、三级评审才能最终通过。5.1.1 审批层级提交人 │ ▼ ┌─────────┐ 通过 ┌─────────┐ 通过 ┌─────────┐ │ 一级审批 │ ───────► │ 二级审批 │ ───────► │ 三级审批 │ ──► 审批通过 └────┬────┘ └────┬────┘ └────┬────┘ │驳回 │驳回 │驳回 ▼ ▼ ▼ 退回提交人 退回上一级 退回上一级5.1.2 数据模型// src/types/approval.tsexportenumApprovalLevel{LEVEL_11,LEVEL_22,LEVEL_33,}exportenumApprovalStatus{PENDINGpending,// 待审批APPROVEDapproved,// 已通过REJECTEDrejected,// 已驳回CANCELLEDcancelled,// 已撤销}exportenumApprovalAction{SUBMITsubmit,// 提交审批APPROVEapprove,// 通过REJECTreject,// 驳回CANCELcancel,// 撤销REASSIGNreassign,// 转办}// 审批流主表exportinterfaceApprovalFlow{id:string;bizId:string;// 业务ID如任务单IDbizType:string;// 业务类型title:string;// 审批标题submitter:string;// 提交人submitterId:string;currentLevel:ApprovalLevel;// 当前审批层级totalLevels:number;// 总审批层级status:ApprovalStatus;// 整体状态createdAt:string;updatedAt:string;completedAt?:string;}// 审批节点记录exportinterfaceApprovalNode{id:string;flowId:string;level:ApprovalLevel;approver:string;approverId:string;status:ApprovalStatus;opinion:string;// 审批意见actionAt?:string;createdAt:string;}// 审批操作日志exportinterfaceApprovalLog{id:string;flowId:string;level:ApprovalLevel;action:ApprovalAction;operator:string;operatorId:string;opinion?:string;fromStatus?:ApprovalStatus;toStatus?:ApprovalStatus;details?:Recordstring,any;createdAt:string;}5.1.3 为什么这样设计审批流主表ApprovalFlow记录整体审批进度一个业务单对应一条审批流审批节点ApprovalNode每一级审批一条记录方便追溯每一级的审批人和意见操作日志ApprovalLog记录所有操作包括提交、通过、驳回、转办、撤销等作为审计依据三表分离的好处职责清晰查询审批进度查 Flow看每级审批详情查 Node审计追溯查 Log。5.2 审批流状态机实现审批流的核心是状态流转。我们用一个状态机来管理审批流的生命周期。5.2.1 状态流转规则// src/utils/approvalStateMachine.tsimport{ApprovalStatus,ApprovalAction,ApprovalLevel}fromtypes/approval;interfaceTransitionContext{currentStatus:ApprovalStatus;currentLevel:ApprovalLevel;totalLevels:number;action:ApprovalAction;operatorRole:submitter|approver|admin;}interfaceTransitionResult{valid:boolean;newStatus?:ApprovalStatus;newLevel?:ApprovalLevel;message?:string;}constlevelLabels:RecordApprovalLevel,string{[ApprovalLevel.LEVEL_1]:一级,[ApprovalLevel.LEVEL_2]:二级,[ApprovalLevel.LEVEL_3]:三级,};exportfunctioncheckApprovalTransition(ctx:TransitionContext):TransitionResult{const{currentStatus,currentLevel,totalLevels,action,operatorRole}ctx;// 只有待审批状态可以操作if(currentStatus!ApprovalStatus.PENDINGaction!ApprovalAction.CANCEL){return{valid:false,message:当前状态不允许此操作};}switch(action){caseApprovalAction.APPROVE:{if(operatorRole!approver){return{valid:false,message:只有审批人可以执行通过操作};}// 如果是最后一级审批流完成if(currentLeveltotalLevels){return{valid:true,newStatus:ApprovalStatus.APPROVED,};}// 否则进入下一级return{valid:true,newLevel:(currentLevel1)asApprovalLevel,newStatus:ApprovalStatus.PENDING,};}caseApprovalAction.REJECT:{if(operatorRole!approver){return{valid:false,message:只有审批人可以执行驳回操作};}return{valid:true,newStatus:ApprovalStatus.REJECTED,};}caseApprovalAction.CANCEL:{if(operatorRole!submitteroperatorRole!admin){return{valid:false,message:只有提交人或管理员可以撤销};}if(currentStatusApprovalStatus.APPROVED||currentStatusApprovalStatus.REJECTED){return{valid:false,message:已完成的审批不能撤销};}return{valid:true,newStatus:ApprovalStatus.CANCELLED,};}caseApprovalAction.REASSIGN:{if(operatorRole!approveroperatorRole!admin){return{valid:false,message:只有审批人或管理员可以转办};}return{valid:true,newStatus:ApprovalStatus.PENDING,// 状态不变};}default:return{valid:false,message:未知操作};}}exportfunctiongetApprovalLevelText(level:ApprovalLevel):string{returnlevelLabels[level]||${level}级;}5.3 审批操作界面5.3.1 审批页面布局┌────────────────────────────────────────────┐ │ 审批详情页 │ ├──────────────┬─────────────────────────────┤ │ │ 审批标题 │ │ 审批进度条 │ 提交人 / 提交时间 │ │(步骤条)├─────────────────────────────┤ │ │ 业务详情嵌入组件 │ │ ├─────────────────────────────┤ │ │ 审批操作区 │ │ │[通过][驳回][转办][撤销]│ │ │ 审批意见输入框 │ └──────────────┴─────────────────────────────┘5.3.2 审批操作面板组件// src/pages/approval/components/ApprovalPanel.tsx import { useState } from react; import { Card, Button, Space, Input, Modal, message, Select } from antd; import { CheckOutlined, CloseOutlined, RollbackOutlined, UserSwitchOutlined } from ant-design/icons; import { ApprovalFlow, ApprovalAction, ApprovalLevel } from types/approval; import { approvalApi } from services/approval; import { checkApprovalTransition, getApprovalLevelText } from utils/approvalStateMachine; const { TextArea } Input; interface Props { flow: ApprovalFlow; currentUserRole: submitter | approver | admin; onSuccess: () void; } export default function ApprovalPanel({ flow, currentUserRole, onSuccess }: Props) { const [opinion, setOpinion] useState(); const [loading, setLoading] useState(false); const canApprove checkApprovalTransition({ currentStatus: flow.status, currentLevel: flow.currentLevel, totalLevels: flow.totalLevels, action: ApprovalAction.APPROVE, operatorRole: currentUserRole, }).valid; const canReject checkApprovalTransition({ currentStatus: flow.status, currentLevel: flow.currentLevel, totalLevels: flow.totalLevels, action: ApprovalAction.REJECT, operatorRole: currentUserRole, }).valid; const canCancel checkApprovalTransition({ currentStatus: flow.status, currentLevel: flow.currentLevel, totalLevels: flow.totalLevels, action: ApprovalAction.CANCEL, operatorRole: currentUserRole, }).valid; const handleAction (action: ApprovalAction, actionLabel: string, needOpinion true) { if (needOpinion !opinion.trim()) { message.warning(请填写审批意见); return; } Modal.confirm({ title: 确认${actionLabel}, content: 确定要${actionLabel}该审批吗, okText: actionLabel, okButtonProps: { danger: action ApprovalAction.REJECT || action ApprovalAction.CANCEL }, onOk: async () { setLoading(true); try { await approvalApi.action(flow.id, { action, opinion }); message.success(${actionLabel}成功); onSuccess(); } catch (e) { // 错误已在拦截器处理 } finally { setLoading(false); } }, }); }; const handleReassign () { let targetUserId ; Modal.confirm({ title: 转办审批, content: ( div p style{{ marginBottom: 8 }}选择转办对象/p Select style{{ width: 100% }} placeholder请选择审批人 onChange{(v) (targetUserId v)} options{[ { label: 张三, value: user1 }, { label: 李四, value: user2 }, { label: 王五, value: user3 }, ]} / TextArea rows{3} placeholder转办意见可选 style{{ marginTop: 12 }} onChange{(e) setOpinion(e.target.value)} / /div ), onOk: async () { if (!targetUserId) { message.warning(请选择转办对象); return Promise.reject(); } setLoading(true); try { await approvalApi.reassign(flow.id, { targetUserId, opinion }); message.success(转办成功); onSuccess(); } finally { setLoading(false); } }, }); }; const showActions canApprove || canReject || canCancel; return ( Card title{${getApprovalLevelText(flow.currentLevel)}审批} sizesmall Space directionvertical sizemiddle style{{ width: 100% }} {showActions ( div div style{{ marginBottom: 8, color: #666 }}审批意见/div TextArea rows{3} placeholder请输入审批意见... value{opinion} onChange{(e) setOpinion(e.target.value)} maxLength{500} showCount / /div Space wrap {canApprove ( Button typeprimary icon{CheckOutlined /} loading{loading} onClick{() handleAction(ApprovalAction.APPROVE, 通过, false)} 通过 /Button )} {canReject ( Button danger icon{CloseOutlined /} loading{loading} onClick{() handleAction(ApprovalAction.REJECT, 驳回)} 驳回 /Button )} {canApprove ( Button icon{UserSwitchOutlined /} onClick{handleReassign} loading{loading} 转办 /Button )} {canCancel ( Button icon{RollbackOutlined /} loading{loading} onClick{() handleAction(ApprovalAction.CANCEL, 撤销, false)} 撤销审批 /Button )} /Space / )} {!showActions ( div style{{ color: #999, textAlign: center, padding: 20px 0 }} 您没有可执行的操作 /div )} /Space /Card ); }5.4 操作审计日志审计日志是审批系统的重要组成部分记录「谁在什么时候做了什么」。5.4.1 日志数据结构我们在前面的类型定义中已经定义了ApprovalLog它包含操作人谁做的操作操作时间什么时候做的操作类型提交 / 通过 / 驳回 / 转办 / 撤销审批层级在哪一级做的审批意见操作时填写的意见状态变更从什么状态变到什么状态扩展字段如转办的目标人等5.4.2 审批时间线组件// src/pages/approval/components/ApprovalTimeline.tsx import { Timeline, Tag, Avatar } from antd; import dayjs from dayjs; import { CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, RollbackOutlined, UserSwitchOutlined, SendOutlined, } from ant-design/icons; import { ApprovalLog, ApprovalAction, ApprovalLevel } from types/approval; import { getApprovalLevelText } from utils/approvalStateMachine; interface Props { logs: ApprovalLog[]; } const actionConfig: RecordApprovalAction, { label: string; color: string; icon: React.ReactNode } { [ApprovalAction.SUBMIT]: { label: 提交审批, color: blue, icon: SendOutlined /, }, [ApprovalAction.APPROVE]: { label: 通过, color: green, icon: CheckCircleOutlined /, }, [ApprovalAction.REJECT]: { label: 驳回, color: red, icon: CloseCircleOutlined /, }, [ApprovalAction.CANCEL]: { label: 撤销, color: default, icon: RollbackOutlined /, }, [ApprovalAction.REASSIGN]: { label: 转办, color: purple, icon: UserSwitchOutlined /, }, }; export default function ApprovalTimeline({ logs }: Props) { const sortedLogs [...logs].sort( (a, b) new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), ); return ( Timeline items{sortedLogs.map((log) { const config actionConfig[log.action] || actionConfig[ApprovalAction.SUBMIT]; return { color: config.color, dot: config.icon, children: ( div div style{{ marginBottom: 4 }} Avatar size{24} style{{ marginRight: 8, verticalAlign: middle }} {log.operator?.[0]} /Avatar span style{{ fontWeight: 500 }}{log.operator}/span Tag color{config.color} style{{ marginLeft: 8 }} {config.label} /Tag Tag style{{ marginLeft: 4 }} {getApprovalLevelText(log.level)} /Tag /div {log.opinion ( div style{{ backgroundColor: #f5f5f5, padding: 8px 12px, borderRadius: 4, marginBottom: 4, marginLeft: 32, color: #666, }} {log.opinion} /div )} div style{{ color: #999, fontSize: 12, paddingLeft: 32 }} {dayjs(log.createdAt).format(YYYY-MM-DD HH:mm:ss)} /div /div ), }; })} / ); }5.5 审批列表页5.5.1 待我审批 / 我发起的 / 我已审批审批列表通常有三个 tab// src/pages/approval/list/index.tsx import { useState } from react; import { Card, Tabs, Table, Tag, Space, Button } from antd; import { useNavigate } from react-router-dom; import dayjs from dayjs; import type { ColumnsType } from antd/es/table; import { ApprovalFlow, ApprovalStatus, ApprovalLevel } from types/approval; import { approvalApi } from services/approval; import { useTable } from hooks/useTable; import { getApprovalLevelText } from utils/approvalStateMachine; const statusConfig: RecordApprovalStatus, { label: string; color: string } { [ApprovalStatus.PENDING]: { label: 待审批, color: gold }, [ApprovalStatus.APPROVED]: { label: 已通过, color: green }, [ApprovalStatus.REJECTED]: { label: 已驳回, color: red }, [ApprovalStatus.CANCELLED]: { label: 已撤销, color: default }, }; export default function ApprovalList() { const navigate useNavigate(); const [activeTab, setActiveTab] useState(pending); const [page, setPage] useState(1); const [pageSize, setPageSize] useState(20); const { dataSource, total, loading, refresh } useTableApprovalFlow, { type: string }({ fetchFn: (params) approvalApi.getList(params), defaultParams: { type: pending }, immediate: true, }); const handleTabChange (key: string) { setActiveTab(key); setPage(1); refresh(); }; const columns: ColumnsTypeApprovalFlow [ { title: 审批标题, dataIndex: title, render: (text, record) ( a onClick{() navigate(/approval/${record.id})}{text}/a ), }, { title: 当前层级, dataIndex: currentLevel, width: 100, render: (level: ApprovalLevel, record) ${getApprovalLevelText(level)} / ${record.totalLevels}级, }, { title: 状态, dataIndex: status, width: 100, render: (status: ApprovalStatus) ( Tag color{statusConfig[status]?.color}{statusConfig[status]?.label}/Tag ), }, { title: activeTab submitted ? 提交人 : 申请人, dataIndex: submitter, width: 100, }, { title: 提交时间, dataIndex: createdAt, width: 160, render: (t) dayjs(t).format(YYYY-MM-DD HH:mm), }, ]; return ( div style{{ padding: 24 }} Card Tabs activeKey{activeTab} onChange{handleTabChange} items{[ { key: pending, label: 待我审批 }, { key: submitted, label: 我发起的 }, { key: approved, label: 我已审批 }, { key: all, label: 全部审批 }, ]} / Table rowKeyid columns{columns} dataSource{dataSource} loading{loading} pagination{{ current: page, pageSize, total, showTotal: (t) 共 ${t} 条, onChange: (p, ps) { setPage(p); setPageSize(ps); }, }} / /Card /div ); }5.6 审批进度步骤条在详情页顶部展示审批进度让用户一眼看清当前审批到了哪一步。// src/pages/approval/components/ApprovalProgress.tsx import { Steps, Avatar } from antd; import { ApprovalFlow, ApprovalNode, ApprovalLevel } from types/approval; import { getApprovalLevelText } from utils/approvalStateMachine; interface Props { flow: ApprovalFlow; nodes: ApprovalNode[]; } export default function ApprovalProgress({ flow, nodes }: Props) { const items Array.from({ length: flow.totalLevels }).map((_, i) { const level i 1; const node nodes.find((n) n.level level); const isCurrent level flow.currentLevel flow.status pending; const isDone level flow.currentLevel || flow.status approved; let status: wait | process | finish | error wait; if (isDone) status finish; else if (isCurrent) status process; if (node?.status rejected) status error; return { title: getApprovalLevelText(level as ApprovalLevel), description: node ? ( div div Avatar size{18} style{{ marginRight: 4 }} {node.approver?.[0]} /Avatar {node.approver} /div {node.actionAt ( div style{{ fontSize: 12, color: #999, marginTop: 2 }} {new Date(node.actionAt).toLocaleString(zh-CN, { hour12: false })} /div )} /div ) : ( span style{{ color: #bbb }}待审批/span ), status, }; }); return Steps current{flow.currentLevel - 1} items{items} /; }本章小结知识点关键内容审批模型三表设计审批流主表 审批节点 操作日志职责分离状态机checkApprovalTransition函数集中管理状态流转规则支持权限校验审批操作通过 / 驳回 / 转办 / 撤销操作前二次确认必填审批意见审计日志记录操作人、时间、操作类型、意见、状态变更完整追溯审批时间线Timeline 组件可视化展示审批全过程审批列表多 Tab 切换待我审批 / 我发起的 / 我已审批 / 全部进度步骤条Steps 组件展示审批进度清晰明了下一章预告我们来实现定时任务管理学习 Cron 表达式原理和可视化编辑器的集成。
返回列表