React 自定义组件:打造一个超灵活的团队成员选择器

在企业应用中,选择组织架构中的成员是个常见需求,比如分配任务、设置权限或组建团队。今天,我要带你认识一个超实用的 React 自定义组件 AutoUserGrup,它能让你轻松从树形组织结构中挑选人员,还支持动态加载和多选功能。想象一下:点击一个部门,右侧弹出所有成员,勾选后还能跨部门混搭选择,简直是“团队拼图”的神器!我们会从代码优化入手,再通过一个“团队成员选择器”的 Demo 展示它的魅力。准备好了吗?让我们一起解锁这个组件的秘密吧!


组件的核心功能:组织 + 人员选择

AutoUserGrup 的任务是:

  1. 展示组织树:左侧显示树形结构(如公司 → 部门 → 小组)。
  2. 动态加载:点击节点时加载子节点或成员列表。
  3. 多选人员:右侧显示选中组织下的成员,支持全选和跨组织选择。
  4. 实时反馈:已选成员以标签形式展示,可随时删除。

它就像一个“组织导航仪”,帮你快速定位并挑选团队成员。


优化后的代码:从“好用”到“优雅”

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;

优化亮点

  1. 类型安全:添加 TypeScript 接口,清晰定义数据结构。
  2. 逻辑简洁:用数组方法替换复杂循环,提升可读性。
  3. 动态加载:异步加载子节点和用户列表更流畅。
  4. 状态管理:优化 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;

使用效果

  1. 点击“技术部”,加载“前端组”和“后端组”。
  2. 点击“前端组”,右侧显示“张三”和“李四”,勾选后顶部出现标签。
  3. 点击“后端组”,勾选“王五”,标签更新为“张三、李四、王五”。
  4. 点击标签上的“x”,移除对应成员。

组件解析:从树到人的魔法

  1. 组织树(左侧)
    • 用 Tree 组件展示树形结构。
    • loadData 动态加载子节点,点击时调用 loadDataFetch。
  2. 成员列表(右侧)
    • 用 Checkbox.Group 显示用户选项。
    • onChange 更新选中状态,同步到 checkedAllList。
  3. 已选标签(顶部)
    • 用 Tag 展示已选成员,支持删除。
    • 通过 checkedAllList 管理跨组织选择。
  4. 数据联动
    • updateTreeDataSource 更新树节点,确保动态数据生效。
    • useEffect 实时通知父组件已选结果。

      使用场景与扩展

      场景

    • 权限分配:为不同部门用户设置权限。
    • 任务指派:从组织中挑选任务执行者。
    • 团队管理:跨部门组建项目团队。
    • 扩展

    • 搜索功能:添加输入框过滤成员。
    • 批量操作:支持按组全选。
    • 自定义渲染:支持头像或更多用户信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值