接到一个审批流程图的需求,就是我们日常OA系统当中的某一种流程,例如请假流程,出差审批流程等,都需要由我们发起,领导逐级审批,最后查看审批结果;
我觉得这是其中一种,是固定死审批人以及流程步骤,还有一种,设定多个流程,当我们选择某一个流程,流程步骤已经定好了,但是人员可以让我们自行选择,每一流程的人员由用户自定义。
这个整体效果是怎么样呢??
小二,给客官上图
需求单子就是这么个需求,下面开始试试水,看这水深不深啊!
需求拆解流程:
- 根据流程不同,每一步的流程内容也不同;
- 选择人员以及删除人员
- 人员信息展示
好像也就三个流程,一个一个来
首先呢,这个流程肯定不是写死,根据数据渲染,其次,从样式上看,其中好像这是一个 步骤条组件,每一步骤下面嵌套着N多个联系人。
组件的选用以及二次封装
- Antd Steps
- Antd Avatar
- Antd Badge
- Antd Button
- AvatarConstructor // 自己封装 头像构造器
Antd Steps
- 步骤条组件可以完成我们每一流程当中的审批节点
<Steps
progressDot // 使得节点为 小圆点
current={stepData.length} // 进度条进行到哪个节点
direction="vertical" // 进度条方向
items={handleTransformData(stepData)} // 每一个节点的数据
/>
- 步骤条数据定义
// 步骤条数据
const stepItems = [
{
title: '测试1',
stepId: 1, // 用来标记每一个节点Id
avatarData: [], // 用来标记每一个节点人员信息
},
{
title: '测试2',
stepId: 2,
avatarData: [],
},
{
title: '结束',
avatarData: null,
stepId: 3,
},
]
- 根据数据去渲染出每一个节点的审批人员信息
// 数据转换 数据格式 根据Antd Step组件要求进去转化
const handleTransformData = (data = []) => {
const res = data?.map((it, idx) => {
// eslint-disable-next-line no-unsafe-optional-chaining
// 这一步是 使得最后一个审批节点无展示内容
if (idx === data?.length - 1) {
return {
title: it.title,
description: null,
}
}
// 其余的节点 根据数据以及我们抽离的 头像构造器组件 完成渲染
return {
title: it.title,
description: creatorAvatar(it.stepId, it.avatarData),
}
})
return res
}
AvatarConstructor
- 每一个审批节点,都会存在审批人员的信息,封装成一个头像组件
- 这里采用到 Antd Avatar && Badge组件
头像构造器
// 头像构造器
const creatorAvatar = (stepId: number, peopleData = []) => {
// 每一个审批人员的卡片
const avatarBox = peopleData?.map(item => {
const { uname, userUnit, imgUrl, pid } = item
return (
<ReviewerCp
uname={uname} // 传进去的用户名
userUnit={userUnit} // 传进去的用户所属单位
imgUrl={imgUrl || ''} // 传进去的用户头像url
pid={pid} // 传进去的用户id
changeOriginData={setStepData} // 传进去的改变整个步骤条数据的 方法 === react setXXX Fn()
originData={stepData} // 传进去的步骤条的数据
stepId={stepId} // 当前用户所属的步骤ID
key={pid} // 唯一标识
/>
)
})
return (
<div className="flex flex-col">
{!!avatarBox?.length && <div className="flex flex-col">{avatarBox}</div>}
// 默认的添加人员按钮
<Button
type="dashed"
icon={<PlusOutlined className="add-icon" />}
shape="circle"
className="mt-2"
size="large"
onClick={() => openSelectPeopleModal(stepId, stepData)}
/>
</div>
)
}
<ReviewerCp/>
<div className="flex items-center">
<div>
<Badge
count={<CloseCircleOutlined onClick={() => handleDelete()} />}
offset={[-5, 5]}
className="avatar-badge"
>
<Avatar
shape={imgType || 'circle'}
size="large"
src={imgUrl || ''}
icon={<UserOutlined />}
className="avatar-img"
/>
</Badge>
</div>
<div className="flex flex-col ml-2">
<div className="uname-text">{uname || '--'}</div>
<div className="user-unit-text">{userUnit || '--'}</div>
</div>
</div>
- 效果展示-人员卡片
- 效果展示-待添加人员状态
节点的人员添加
- 添加人员
- 逻辑:当用户点击添加按钮的时候,我们需要去拿到一些信息
- 首先,选中节点的id ==> stepId
- 其次,获取当前节点人员数据 有则追加,无则添加
// 添加人员
const handleAddApprover = (stepId: number, peopleData = [], addPeopleData = []) => {
const tempData = peopleData
const tempAddData = []
// 这里只是为了判断 一次添加多个或者单个 是否生效
if (Math.random() > 0.5) {
tempAddData.push(
...[
{
uname: `小面33${Math.random()}`,
userUnit: `餐厅344${Math.random()}`,
pid: parseInt(`${Math.random() * 100}`, 10),
},
{
uname: `小面33${Math.random()}`,
userUnit: `餐厅344${Math.random()}`,
pid: parseInt(`${Math.random() * 100}`, 10),
},
]
)
} else {
tempAddData.push(
...[
{
uname: `小面33${Math.random()}`, // 头像组件需要展示的信息 用户名
userUnit: `餐厅344${Math.random()}`, // 头像组件需要展示的信息 用户所属单位
pid: parseInt(`${Math.random() * 100}`, 10), // 人员id,删除需要
},
]
)
}
peopleData?.forEach((item, idx) => {
if (item.stepId === stepId) {
let addResData = []
if (item?.avatarData) {
addResData = item?.avatarData
addResData.push(...tempAddData)
} else {
addResData.push(...tempAddData)
}
tempData.splice(idx, 1, Object.assign(item, { avatarData: addResData }))
}
})
setStepData([...tempData])
}
- 效果展示-添加人员
节点的人员删除
- 删除单个节点的人员
- 逻辑:当用户点击头像上的 Badge 的时候,我们需要去拿到一些信息
- 首先,拿到用户点击头像的这一节点id ==> stepId
- 其次,用户想删除的 pid
- 数据操作,把我们不想要的去除
const handleDelete = () => {
const tempData = originData // 备份一份原数据
originData?.forEach((item, index) => {
// 找到当前的步骤ID
if (item.stepId === stepId) {
// 拿出当前步骤ID的人员数据
const tempAvatarData = item.avatarData
// 根据用户删除的OD 去过滤出剩余的人员ID
const resAvatarData = tempAvatarData?.filter(it => it.pid !== pid)
// 重新set数据进去 数组的 splice
tempData.splice(index, 1, Object.assign(item, { avatarData: resAvatarData }))
}
})
// react 当中 setData Fn()
changeOriginData([...tempData])
}
- 效果展示-删除人员