在企业应用中,选择组织架构中的成员是个常见需求,比如分配任务、设置权限或组建团队。今天,我要带你认识一个超实用的 React 自定义组件 AutoUserGrup,它能让你轻松从树形组织结构中挑选人员,还支持动态加载和多选功能。想象一下:点击一个部门,右侧弹出所有成员,勾选后还能跨部门混搭选择,简直是“团队拼图”的神器!我们会从代码优化入手,再通过一个“团队成员选择器”的 Demo 展示它的魅力。准备好了吗?让我们一起解锁这个组件的秘密吧!
组件的核心功能:组织 + 人员选择
AutoUserGrup 的任务是:
- 展示组织树:左侧显示树形结构(如公司 → 部门 → 小组)。
- 动态加载:点击节点时加载子节点或成员列表。
- 多选人员:右侧显示选中组织下的成员,支持全选和跨组织选择。
- 实时反馈:已选成员以标签形式展示,可随时删除。
它就像一个“组织导航仪”,帮你快速定位并挑选团队成员。
优化后的代码:从“好用”到“优雅”
1. updateTreeDataSource:树形更新的小能手
interface TreeDataNode {
key: React.Key;
title: string;
children?: TreeDataNode[];
isLeaf?: boolean;
userOption?: UserItem[];
}
function updateTreeDataSource(treeSource: TreeDataNode[], eventKey: React.Key, newChildren: TreeDataNode[] | TreeDataNode): boolean {
return treeSource.some((node, i) => {
if (node.key === eventKey) {
treeSource[i] = { ...node, children: Array.isArray(newChildren) ? newChildren : [newChildren] };
return true;
}
if (node.children) {
return updateTreeDataSource(node.children, eventKey, newChildren);
}
return false;
});
}
优化亮点:
- 用 some 替换 for 循环,逻辑更简洁。
- 支持单个节点或数组更新,灵活性更强。
- 返回布尔值表示是否更新成功。
2. AutoUserGrup 组件
import React, { useState, useEffect } from 'react';
import { Tree, Checkbox, Tag } from 'antd';
import { CheckboxProps } from 'antd/es/checkbox';
import { cloneDeep } from 'lodash';
import styles from './AutoUserGrup.module.css';
interface UserItem {
userId: string;
label: string;
value: string;
}
interface Props {
loadUserFetch: (params: { orgId: string }) => Promise<UserItem[]>;
loadDataFetch: (params: { parentId: string }) => Promise<any[]>;
treeData?: TreeDataNode[];
userList?: UserItem[];
selectedData?: { userId: string; userName: string }[];
type?: string;
handleChange?: (data: { list: { userId: string }[]; type?: string }) => void;
}
const AutoUserGrup: React.FC<Props> = ({
loadUserFetch,
loadDataFetch,
treeData = [],
userList = [],
selectedData = [],
type,
handleChange,
}) => {
const [userTreeData, setUserTreeData] = useState<TreeDataNode[]>(treeData);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const [checkedAllList, setCheckedAllList] = useState<UserItem[]>(userList);
const [checkedList, setCheckedList] = useState<string[]>([]);
const [userOption, setUserOption] = useState<UserItem[]>([]);
const checkAll = userOption.length > 0 && userOption.length === checkedList.length;
const indeterminate = checkedList.length > 0 && checkedList.length < userOption.length;
const onChange = (list: string[]) => {
const selected = userOption.filter(i => list.includes(i.userId));
setCheckedAllList(prev => {
const newList = cloneDeep(prev);
selected.forEach(i => {
if (!newList.some(r => r.userId === i.userId)) newList.push(i);
});
return newList.filter(r => list.includes(r.userId) || !userOption.some(u => u.userId === r.userId));
});
setCheckedList(list);
};
const onCheckAllChange: CheckboxProps['onChange'] = e => {
const newList = e.target.checked
? [...checkedAllList, ...userOption.filter(i => !checkedAllList.some(r => r.userId === i.userId))]
: checkedAllList.filter(r => !userOption.some(u => u.userId === r.userId));
setCheckedAllList(newList);
setCheckedList(e.target.checked ? userOption.map(i => i.userId) : []);
};
const loadData = (treeNode: any) =>
loadDataFetch({ parentId: treeNode.id }).then(children =>
children.map((i: any) => ({
key: i.id,
title: i.name,
isLeaf: i.leaf,
}))
);
const handleSelect = async (_: React.Key[], info: any) => {
const key = info.node.key as string;
setTargetKeys([key]);
let options = info.node.userOption;
if (!options) {
const users = (await loadUserFetch({ orgId: key })) || [];
options = users.map((i: any) => ({ userId: i.id, label: i.name, value: i.id }));
updateTreeDataSource(userTreeData, key, { ...info.node, userOption: options });
setUserTreeData([...userTreeData]);
}
setUserOption(options);
setCheckedList(options.filter(r => checkedAllList.some(i => i.userId === r.userId)).map(r => r.userId));
};
useEffect(() => {
if (handleChange) {
handleChange({ list: checkedAllList.map(i => ({ userId: i.value })), type });
}
}, [checkedAllList, handleChange, type]);
useEffect(() => {
if (selectedData.length) {
setCheckedAllList(selectedData.map(i => ({ userId: i.userId, label: i.userName, value: i.userId })));
}
}, [selectedData]);
return (
<div className={styles.AutoUserGrup}>
<div className={styles.topCon}>
{checkedAllList.map(i => (
<Tag
key={i.userId}
closable
onClose={() => setCheckedAllList(prev => prev.filter(r => r.userId !== i.userId))}
>
{i.label}
</Tag>
))}
</div>
<div className={styles.con}>
<div className={styles.leftTree}>
<Tree treeData={userTreeData} loadData={loadData} onSelect={handleSelect} />
</div>
<div className={styles.rightRadio}>
<Checkbox indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
全选用户
</Checkbox>
<Checkbox.Group value={checkedList} onChange={onChange}>
<div className={styles.radioItem}>
{userOption.length === 0 ? (
<div>暂无用户</div>
) : (
userOption.map(i => (
<Checkbox key={i.userId} value={i.userId}>
{i.label}
</Checkbox>
))
)}
</div>
</Checkbox.Group>
</div>
</div>
</div>
);
};
export default AutoUserGrup;
优化亮点:
- 类型安全:添加 TypeScript 接口,清晰定义数据结构。
- 逻辑简洁:用数组方法替换复杂循环,提升可读性。
- 动态加载:异步加载子节点和用户列表更流畅。
- 状态管理:优化 checkedAllList 和 checkedList 的更新逻辑。
Demo:团队成员选择器
让我们用一个“团队成员选择器”展示这个组件。左侧是公司组织树,右侧是成员列表,顶部显示已选成员。
使用示例
import React from 'react';
import AutoUserGrup from './AutoUserGrup';
const mockTreeData = [
{ key: '1', title: '技术部', children: [] },
{ key: '2', title: '销售部', children: [] },
];
const mockLoadDataFetch = async ({ parentId }: { parentId: string }) => {
if (parentId === '1') {
return [
{ id: '1-1', name: '前端组', leaf: true },
{ id: '1-2', name: '后端组', leaf: true },
];
}
return [];
};
const mockLoadUserFetch = async ({ orgId }: { orgId: string }) => {
if (orgId === '1-1') {
return [
{ id: 'u1', name: '张三' },
{ id: 'u2', name: '李四' },
];
}
if (orgId === '1-2') {
return [
{ id: 'u3', name: '王五' },
{ id: 'u4', name: '赵六' },
];
}
return [];
};
const TeamSelector: React.FC = () => {
const handleChange = (data: any) => {
console.log('已选成员:', data.list);
};
return (
<div style={{ padding: '20px' }}>
<h2>团队成员选择器</h2>
<AutoUserGrup
treeData={mockTreeData}
loadDataFetch={mockLoadDataFetch}
loadUserFetch={mockLoadUserFetch}
handleChange={handleChange}
/>
</div>
);
};
export default TeamSelector;
使用效果
- 点击“技术部”,加载“前端组”和“后端组”。
- 点击“前端组”,右侧显示“张三”和“李四”,勾选后顶部出现标签。
- 点击“后端组”,勾选“王五”,标签更新为“张三、李四、王五”。
- 点击标签上的“x”,移除对应成员。
组件解析:从树到人的魔法
- 组织树(左侧):
- 用 Tree 组件展示树形结构。
- loadData 动态加载子节点,点击时调用 loadDataFetch。
- 成员列表(右侧):
- 用 Checkbox.Group 显示用户选项。
- onChange 更新选中状态,同步到 checkedAllList。
- 已选标签(顶部):
- 用 Tag 展示已选成员,支持删除。
- 通过 checkedAllList 管理跨组织选择。
- 数据联动:
- updateTreeDataSource 更新树节点,确保动态数据生效。
- useEffect 实时通知父组件已选结果。
使用场景与扩展
场景:
- 权限分配:为不同部门用户设置权限。
- 任务指派:从组织中挑选任务执行者。
- 团队管理:跨部门组建项目团队。
-
扩展:
- 搜索功能:添加输入框过滤成员。
- 批量操作:支持按组全选。
- 自定义渲染:支持头像或更多用户信息