• 使用Ant Design的Table和Checkbox模拟Tree


    一、小功能大需求

      先看下设计图:

       

      需求如下:

        1、一级选中(取消选中),该一级下的二级全部选中(取消选中)

      2、二级全选,对应的一级选中,二级未全选中,对应的一级不选中

      3、支持搜索,只搜索二级数据,并且只展示搜索到的数据以及对应的一级title,如:搜索“店员”,此时一级只展示咖啡厅....其他一级隐藏,二级只展示店员,其他二级隐藏

      4、搜索出来的数据,一级不可选中,即不允许全选,搜索框清空时,回归初始化状态

      5、搜索后,自动展开所有二级,默认情况下收起所有二级

      看到图的时候,第一反应就是使用Tree就能搞定,但是翻阅了文档后,发现Tree并不能全部完成,所以就只能使用其他组件进行拼装,最后发现使用Table和Checkbox可以完美实现。

    二、逐步完成需求

      如果不想看这些,可直接到最后,有完整代码。。。。。。

      1、页面构建

      这个就不用多说,只是一个简单的Table嵌套Checkbox,具体可去查看文档,直接贴代码,因为是布局,所有可以忽略代码中的事件。

      注意一点:因为搜索时,会改变数据,所以需要将初始化的数据进行保存

    import React, { useState, useRef, useEffect } from "react";
    import { Table, Input, Checkbox } from "antd";
    const { Search } = Input;
    
    export default () => {
      const initialData: any = useRef([]);  //使用useRef创建initialData
      const [data, setData] = useState([
        {
          key: 1,
          title: "普通餐厅(中餐/日料/西餐厅)",
          checkboxData: [
            { key: 12, title: "普通服务员" },
            { key: 13, title: "收银" },
            { key: 14, title: "迎宾/接待" },
          ],
        },
        {
          key: 2,
          title: "零售/快消/服装",
          checkboxData: [
            { key: 17, title: "基础店员" },
            { key: 19, title: "收银员" },
            { key: 20, title: "理货员" },
          ],
        },
      ]);
      useEffect(() => {
        initialData.current = [...data]; //设置初始化值
      }, []);
      const [checkedJob, setCheckedJob] = useState([]); //设置子级中选择的类
      const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]); //设置选择的行
      const expandedRowRender = (record: any) => {
        return (
          <div style={{ paddingLeft: 50, boxSizing: "border-box" }}>
            <p>请选择岗位,或勾选类别全选岗位</p>
            <div>
              <Checkbox.Group value={checkedJob}>
                {record.checkboxData.map((item: any) => {
                  return (
                    <Checkbox
                      value={item.key}
                      key={item.key}
                      onChange={checkChange}
                    >
                      {item.title}
                    </Checkbox>
                  );
                })}
              </Checkbox.Group>
            </div>
          </div>
        );
      };
      const rowSelection = {
        selectedRowKeys,
      };
      return (
        <div
          style={{
            background: "#fff",
            padding: 24,
            boxSizing: "border-box",
             982,
          }}
        >
          <Search
            placeholder="请输入岗位名称"
            onSearch={(value) => {
              console.log(loop(value));
            }}
          />
          <Table
            showHeader={false}
            columns={columns}
            expandable={{
              expandedRowRender,
            }}
            dataSource={data}
            pagination={false}
            rowSelection={rowSelection}
          />
        </div>
      );
    };
    const columns = [{ title: "title", dataIndex: "title", key: "title" }];

      2、一级选中(取消全选)

      当一级选中(取消全选)时,需要更新对应二级选项的状态。在antd文档中,使用rowSelection的onSelect,可以设置选择/取消选择某行的回调。

      onSelect:(record,selected)=> record:操作当前行的数据,selected:true:全选,false:取消全选

      注意:当全选时,不能直接添加当前一级下的所有二级,需要过滤掉当前已经选中的二级

      具体逻辑如下代码:

    //首选在rowSelection配置中添加onSelect
    const rowSelection = { selectedRowKeys, onSelect }; //一级全选或者取消的逻辑 const onSelect = (record: any, selected: any) => { //因为存在搜索,所以需要使用我们的初始化数据,找到当前record.key在初始化数据中对应的数据 let initialParent = initialData.current.find( (d: any) => d.key === record.key );
      //初始化数据中对应的二级数据 let selectParentData = initialParent.checkboxData ? initialParent.checkboxData.map((d: any) => d.key) : []; if (selected) { //全选 //向selectRowKeys添加选中的值 setSelectedRowKeys([...selectedRowKeys, record.key]); //更新child数组,将selectParentData中的数据全部过滤添加 setCheckedJob(Array.from(new Set([...checkedJob, ...selectParentData]))); } else { //取消全选 //从父级数组中移除key值 setSelectedRowKeys( [...selectedRowKeys].filter((d: any) => d !== record.key) ); //更新child数组,将selectParentData中的数据全部过滤掉 let newArr: any = []; [...checkedJob].forEach((v) => { if (selectParentData.indexOf(v) === -1) { newArr.push(v); } }); setCheckedJob(newArr); } };

      3、二级选中或取消选中逻辑

      二级选中或者取消比较简单,只要注意在选中时,如何去考虑是否所有二级全部选中即可。具体代码如下。

     //判断b数组中的数据是否全部在a数组中
    const isContained = (a: any, b: any) => { if (!(a instanceof Array) || !(b instanceof Array)) return false; if (a.length
    < b.length) return false; var aStr = a.toString(); for (var i = 0, len = b.length; i < len; i++) { if (aStr.indexOf(b[i]) == -1) return false; } return true; }; //设置checkbox的onChange事件 const checkChange = (e: any) => { let praentRowsKey: any; //找到选中的二级对应的父级key initialData.current.forEach((v: any) => { if (v.checkboxData.find((d: any) => d.key === e.target.value)) { praentRowsKey = v.key; } }); if (e.target.checked) { //选中时 设置当前的check数组 let newCheckedJob = [...checkedJob, e.target.value]; setCheckedJob(newCheckedJob); //判断当前二级的内容是否全部被选中,如果全部选中,则需要设置selectedRowKeys //praentRowsKey下的所有子元素 let childArr = initialData.current .find((d: any) => d.key === praentRowsKey) ?.checkboxData?.map((i: any) => i.key); // 为当前选择之后的新数组 if (isContained(newCheckedJob, childArr)) { //全部包含,设置父级 setSelectedRowKeys([...selectedRowKeys, praentRowsKey]); } } else { //取消选中 设置当前的child数组 setCheckedJob( [...checkedJob].filter((d: number) => d !== e.target.value) ); //判断当前父级中是否存在praentRowsKey,存在则去除 if (!!~selectedRowKeys.indexOf(praentRowsKey)) { setSelectedRowKeys( [...selectedRowKeys].filter((d: any) => d !== praentRowsKey) ); } } };

      4、搜索过滤

      前3步骤完成后,目前来说,正常的一级二级联动已经完成,现在进行第4步,搜索过滤。

      简单的说,搜索的时候,只要改变我们的data,就可以重新渲染Table,这样就可以达成搜索过滤的效果。具体代码如下  

    //Search组件搜索时,触发更改data
    <
    Search placeholder="请输入岗位名称" onSearch={(value) => { setData(loop(value)); }} />

    //搜索岗位时,进行过滤 const loop = (searchValue: any) => { let loopData = initialData.current?.map((item: any) => {
        //判断一级是否包含该搜索内容
        let parentKey = !!~item.title.indexOf(searchValue); let childrenData: any = []; if (item.checkboxData) { //如果存在二级,则进行二级的循环,过滤出搜索到的value childrenData = item.checkboxData.filter( (d: any) => !!~d.title.indexOf(searchValue) ); }
    //如果一级有,二级没有,则展示一级下所有的二级
        //如果一级没有,二级有,则只展示存在的二级以及对应的一级
       //如果一级有,二级有,则展示存在的二级以及对应的一级
    //如果一级没有,二级也没有,则不展示 if(parentKey&&!childrenData.length){
       return {
            title:item.title,
            key:item.key,
            checkboxData:item.checkboxData
          }
       }else if((!parentKey || parentKey)&&childrenData.length){
          return{
            title:item.title,
            key:item.key,
            checkboxData:childrenData
          }
        }else{
        } });   //搜索的值不为空时,返回搜索过滤后都数据(因为map出来的数据中有undefined,所以需要再次进行过滤),为空时返回初始化数据 return searchValue ? loopData.filter((d: any) => d) : initialData.current; };

      5、搜索后,禁止一级全选和取消全选

      动态控制table的选择功能,需要使用rowSelectiongetCheckboxProps。具体代码如下。

    const [selectAllDisabled, setSelectAllDisabled] = useState<boolean>(false); //声明一个变量,控制是否允许选择,默认为false
    
    //在rowSelection中添加getCheckboxProps const rowSelection = { selectedRowKeys, onSelect, getCheckboxProps: (record: any) => ({ disabled: selectAllDisabled,  //true:禁止,false:允许 }), }; //在搜索的时候设置 const loop = (searchValue: any) => { ... setSelectAllDisabled(searchValue ? true : false); //当搜索内容为空时,因为回到的是初始值,所以需要它允许选择,搜索内容不为空时,禁止选择 ... };

      6、设置自动展开

      前5步完成后,如果不需要设置自动展开,则该功能就可以到此结束。

      设置自动展开,需要用到expandable中的onExpand以及expandedRowKeys

      expandedRowKeys:展开的行,控制属性

      onExpand:点击展开图标时触发,(expanded,record)=> expanded:true:展开,false:收起。record:操作的当前行的数据

      具体代码如下:

    const [expandedRowKeys, setExpandedRowKeys] = useState<any>([]); //声明变量设置展开的行,默认全都收起
    
    //table的 expandable添加 onExpand,expandedRowKeys
    <Table expandable={{ expandedRowRender, onExpand, expandedRowKeys, }} />
    //搜索时改变状态 const loop = (searchValue: any) => { ... //有数据时自动展开所有搜索到的,无数据的时候默认全部收起 setExpandedRowKeys( searchValue ? initialData.current.map((d: any) => d.key) : [] ); ... }; //控制表格的展开收起 const onExpand = (expanded: any, record: any) => { if (expanded) { setExpandedRowKeys([...expandedRowKeys, record.key]); //展开时,将需要展开的key添加到数组中 } else { setExpandedRowKeys( [...expandedRowKeys].filter((d: any) => d !== record.key)  //收起时,将该key移除数组 ); } };

    三、优化

      一级选择框有三种状态,全选,二级选中某些个,未选中,三种状态对应不同的样式,如下图所示。

                    

       这种优化,就需要设置rowSelection的renderCell(注意,rendercell在antd的4.1+版本才能生效),配合Checkbox进行更改。具体代码如下。

      1、设置renderCell

      将我们在第二步和第五步设置的onSelect以及getCheckboxProps隐藏,再配置renderCell

    const rowSelection = {
        selectedRowKeys,
        // onSelect,
        // getCheckboxProps: (record: any) => ({
        //   disabled: selectAllDisabled,
        // }),
        renderCell: (checked: any, record: any) => {
          //当前record.key对应大初始化数据的一级所有数据
          let parentArr = initialData?.current?.find(
            (d: any) => d.key === record.key
          );
          //从所有已经选择过的数据中过滤出在parentArr中的数据
          let checkArr = parentArr?.checkboxData?.filter(
            (item: any) => checkedJob.indexOf(item.key) > -1
          );
          return (
            <Checkbox
              indeterminate={
                parentArr?.checkboxData &&
                !!checkArr?.length &&
                checkArr.length < parentArr.checkboxData.length
                  ? true
                  : false
              } //比较 当过滤后选中数据的长度 < 初始化数据的长度时,设置 indeterminate 状态为true,否则为false
              onClick={(e) => onClick(e, record)}
              checked={checked}
              disabled={selectAllDisabled}
            ></Checkbox>
          );
        },
      };

      2、设置onClick事件

      onClick事件其实就是原来的onSelect,具体代码如下

    const onClick = (e: any, record: any) => {
        //存在搜索时,需要进行处理selectParentData
        let initialParent = initialData.current.find(
          (d: any) => d.key === record.key
        );
        let selectParentData = initialParent.checkboxData
          ? initialParent.checkboxData.map((d: any) => d.key)
          : [];
        if (e.target.checked) {
          //向选中数组中添加key值
          setSelectedRowKeys([...selectedRowKeys, record.key]);
          //更新child数组,将selectParentData中的数据全部过滤添加
          setCheckedJob(Array.from(new Set([...checkedJob, ...selectParentData])));
        } else {
          //从父级数组中移除key值
          setSelectedRowKeys(
            [...selectedRowKeys].filter((d: any) => d !== record.key)
          );
          //更新child数组,将selectParentData中的数据全部过滤掉
          let newArr: any = [];
          [...checkedJob].forEach((v) => {
            if (selectParentData.indexOf(v) === -1) {
              newArr.push(v);
            }
          });
          setCheckedJob(newArr);
        }
      }; 

    四、完整代码

    import React, { useState, useRef, useEffect } from "react";
    import { Table, Input, Checkbox } from "antd";
    const { Search } = Input;
    
    export default () => {
      const initialData: any = useRef([]);
      const [data, setData] = useState([
        {
          key: 1,
          title: "普通餐厅(中餐/日料/西餐厅)",
          checkboxData: [
            { key: 12, title: "普通服务员" },
            { key: 13, title: "收银" },
            { key: 14, title: "迎宾/接待" },
          ],
        },
        {
          key: 2,
          title: "零售/快消/服装",
          checkboxData: [
            { key: 17, title: "基础店员" },
            { key: 19, title: "收银员" },
            { key: 20, title: "理货员" },
          ],
        },
      ]);
      useEffect(() => {
        initialData.current = [...data]; //设置初始化值
      }, []);
    
      const [checkedJob, setCheckedJob] = useState([12]); //设置选择的二级
      const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]); //设置选择的行
      const [expandedRowKeys, setExpandedRowKeys] = useState<any>([]); //设置展开的行
      const [selectAllDisabled, setSelectAllDisabled] = useState<boolean>(false); //选择的时候,禁止全选
      //搜索岗位时,进行过滤
      const loop = (searchValue: any) => {
        let loopData = initialData.current?.map((item: any) => {
          let parentKey = !!~item.title.indexOf(searchValue);
          let childrenData: any = [];
          if (item.checkboxData) {
            //如果存在二级,则进行二级的循环,过滤出搜索到的value
            childrenData = item.checkboxData.filter(
              (d: any) => !!~d.title.indexOf(searchValue)
            );
          }
          //1.如果一级有,二级没有,则展示一级下所有的二级
          //2.如果一级没有,二级有,则只展示存在的二级以及对应的一级
          //3.如果一级有,二级有,则展示则存在的二级以及对应的一级
          //4.如果一级没有,二级也没有,则不展示
          if (parentKey && !childrenData.length) {
            return {
              title: item.title,
              key: item.key,
              checkboxData: item.checkboxData,
            };
          } else if ((!parentKey || parentKey) && childrenData.length) {
            return {
              title: item.title,
              key: item.key,
              checkboxData: childrenData,
            };
          } else {
          }
        });
        setSelectAllDisabled(searchValue ? true : false);
        //有数据时自动展开所有搜索到的,无数据的时候默认全部收起
        setExpandedRowKeys(
          searchValue ? initialData.current.map((d: any) => d.key) : []
        );
        return searchValue ? loopData.filter((d: any) => d) : initialData.current;
      };
    
      const isContained = (a: any, b: any) => {
        if (!(a instanceof Array) || !(b instanceof Array)) return false;
        if (a.length < b.length) return false;
        var aStr = a.toString();
        for (var i = 0, len = b.length; i < len; i++) {
          if (aStr.indexOf(b[i]) == -1) return false;
        }
        return true;
      };
      const checkChange = (e: any) => {
        let praentRowsKey: any;
        //找到点击child到一级key
        initialData.current.forEach((v: any) => {
          if (v.checkboxData.find((d: any) => d.key === e.target.value)) {
            praentRowsKey = v.key;
          }
        });
        if (e.target.checked) {
          //选中时 设置当前的child数组
          let newCheckedJob = [...checkedJob, e.target.value];
          setCheckedJob(newCheckedJob);
          //判断当前child的内容是否全部被选中,如果全部选中,则需要设置selectedRowKeys
          //praentRowsKey下的所有子元素
          let childArr = initialData.current
            .find((d: any) => d.key === praentRowsKey)
            ?.checkboxData?.map((i: any) => i.key);
          //  为当前选择之后的新数组
          if (isContained(newCheckedJob, childArr)) {
            //全部包含,设置父级
            setSelectedRowKeys([...selectedRowKeys, praentRowsKey]);
          }
        } else {
          //取消选中 设置当前的child数组
          setCheckedJob(
            [...checkedJob].filter((d: number) => d !== e.target.value)
          );
          //判断当前父级中是否存在praentRowsKey,存在则去除
          if (!!~selectedRowKeys.indexOf(praentRowsKey)) {
            setSelectedRowKeys(
              [...selectedRowKeys].filter((d: any) => d !== praentRowsKey)
            );
          }
        }
      };
    
      //父节点变化时,进行的操作
      //   const onSelect = (record: any, selected: any) => {
      //     //存在搜索时,需要进行处理selectParentData
      //     let initialParent = initialData.current.find(
      //       (d: any) => d.key === record.key
      //     );
      //     let selectParentData = initialParent.checkboxData
      //       ? initialParent.checkboxData.map((d: any) => d.key)
      //       : [];
      //     if (selected) {
      //       //向选中数组中添加key值
      //       setSelectedRowKeys([...selectedRowKeys, record.key]);
      //       //更新child数组,将selectParentData中的数据全部过滤添加
      //       setCheckedJob(Array.from(new Set([...checkedJob, ...selectParentData])));
      //     } else {
      //       //从父级数组中移除key值
      //       setSelectedRowKeys(
      //         [...selectedRowKeys].filter((d: any) => d !== record.key)
      //       );
      //       //更新child数组,将selectParentData中的数据全部过滤掉
      //       let newArr: any = [];
      //       [...checkedJob].forEach((v) => {
      //         if (selectParentData.indexOf(v) === -1) {
      //           newArr.push(v);
      //         }
      //       });
      //       setCheckedJob(newArr);
      //     }
      //   };
    
      //控制表格的展开收起
      const onExpand = (expanded: any, record: any) => {
        //expanded: true展开,false:关闭
        if (expanded) {
          setExpandedRowKeys([...expandedRowKeys, record.key]);
        } else {
          setExpandedRowKeys(
            [...expandedRowKeys].filter((d: any) => d !== record.key)
          );
        }
      };
    
      const onClick = (e: any, record: any) => {
        //存在搜索时,需要进行处理selectParentData
        let initialParent = initialData.current.find(
          (d: any) => d.key === record.key
        );
        let selectParentData = initialParent.checkboxData
          ? initialParent.checkboxData.map((d: any) => d.key)
          : [];
        if (e.target.checked) {
          //向选中数组中添加key值
          setSelectedRowKeys([...selectedRowKeys, record.key]);
          //更新child数组,将selectParentData中的数据全部过滤添加
          setCheckedJob(Array.from(new Set([...checkedJob, ...selectParentData])));
        } else {
          //从父级数组中移除key值
          setSelectedRowKeys(
            [...selectedRowKeys].filter((d: any) => d !== record.key)
          );
          //更新child数组,将selectParentData中的数据全部过滤掉
          let newArr: any = [];
          [...checkedJob].forEach((v) => {
            if (selectParentData.indexOf(v) === -1) {
              newArr.push(v);
            }
          });
          setCheckedJob(newArr);
        }
      };
      const expandedRowRender = (record: any) => {
        return (
          <div style={{ paddingLeft: 50, boxSizing: "border-box" }}>
            <p>请选择岗位,或勾选类别全选岗位</p>
            <div>
              <Checkbox.Group value={checkedJob}>
                {record.checkboxData.map((item: any) => {
                  return (
                    <Checkbox
                      value={item.key}
                      key={item.key}
                      onChange={checkChange}
                    >
                      {item.title}
                    </Checkbox>
                  );
                })}
              </Checkbox.Group>
            </div>
          </div>
        );
      };
      const rowSelection = {
        selectedRowKeys,
        // onSelect,
        // getCheckboxProps: (record: any) => ({
        //   disabled: selectAllDisabled,
        // }),
        renderCell: (checked: any, record: any) => {
          //当前record.key对应大初始化数据的一级所有数据
          let parentArr = initialData?.current?.find(
            (d: any) => d.key === record.key
          );
          //从所有已经选择过的数据中过滤出在parentArr中的数据
          let checkArr = parentArr?.checkboxData?.filter(
            (item: any) => checkedJob.indexOf(item.key) > -1
          );
          return (
            <Checkbox
              indeterminate={
                parentArr?.checkboxData &&
                !!checkArr?.length &&
                checkArr.length < parentArr.checkboxData.length
                  ? true
                  : false
              } //比较 当过滤后选中数据的长度 < 初始化数据的长度时,设置 indeterminate 状态为true,否则为false
              onClick={(e) => onClick(e, record)}
              checked={checked}
              disabled={selectAllDisabled}
            ></Checkbox>
          );
        },
      };
      return (
        <div
          style={{
            background: "#fff",
            padding: 24,
            boxSizing: "border-box",
             982,
          }}
        >
          <Search
            placeholder="请输入岗位名称"
            onSearch={(value) => {
              console.log(loop(value));
              setData(loop(value));
            }}
          />
          <Table
            showHeader={false}
            columns={columns}
            expandable={{
              expandedRowRender,
              onExpand,
              expandedRowKeys,
            }}
            dataSource={data}
            pagination={false}
            rowSelection={rowSelection}
          />
        </div>
      );
    };
    const columns = [{ title: "title", dataIndex: "title", key: "title" }];
    Table+Checkbox模拟Tree完整代码

      

      

      

      

  • 相关阅读:
    关于Python的装饰器(1)
    Linux环境配置备忘
    Spark实施备忘
    RuntimeError: Python is not installed as a framework 错误解决方案
    使用正则表达式验证素数
    Linux终极shell-zsh的完美配置方案!——oh-my-zsh
    终极之shell-zsh全解析
    linux下IPTABLES配置详解
    一款堪称完美的编程字体Source Code Pro
    Linux中一个文件10行内容,如何输出5-8内容到屏幕
  • 原文地址:https://www.cnblogs.com/minorf/p/13706478.html
Copyright © 2020-2023  润新知