1. 程式人生 > 實用技巧 >使用Ant Design的Table和Checkbox模擬Tree

使用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", width: 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 childrenData: any = []; if (item.checkboxData) { //如果存在二級,則進行二級的迴圈,過濾出搜尋到的value childrenData = item.checkboxData.filter( (d: any) => !!~d.title.indexOf(searchValue) ); } if (childrenData.length) { return { title: item.title, key: item.key, checkboxData: childrenData, }; } });   //搜尋的值不為空時,返回搜尋過濾後都資料(因為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); //選擇的時候,禁止全選
  console.log(checkedJob);
  //搜尋崗位時,進行過濾
  const loop = (searchValue: any) => {
    let loopData = initialData.current?.map((item: any) => {
      let childrenData: any = [];
      if (item.checkboxData) {
        //如果存在二級,則進行二級的迴圈,過濾出搜尋到的value
        childrenData = item.checkboxData.filter(
          (d: any) => !!~d.title.indexOf(searchValue)
        );
      }
      if (childrenData.length) {
        return {
          title: item.title,
          key: item.key,
          checkboxData: childrenData,
        };
      }
    });
    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",
        width: 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完整程式碼