實現(xiàn)一個Table泛型組件

Table 組件

可以先看代碼, 代碼查看

表格組件在我們開發(fā)工作中是最常用的整理數(shù)據(jù)的一種組件, 會在很多頁面用到, 我們不可能每個頁面都用 table tr td 這些屬性寫一個表格, 那我們改如何寫一個通用的表格組件呢? 我們從一個最基本的表格慢慢完善一個功能強(qiáng)大的表格

我們思考一下如何讓用戶更好的使用, 我們可以做哪些簡單的工作?

  1. 用戶可以使用 基本表格
  2. 用戶可以設(shè)置 邊框/緊湊/條紋表格
  3. 用戶可以設(shè)置序號列, 不需要自己在 columns 里面設(shè)置
  4. 用戶可以自定義設(shè)置單元格需要展示的數(shù)據(jù)
  5. 用戶可以設(shè)置選擇框, 獲取表格數(shù)據(jù)
  6. 用戶可以根據(jù)列排序 (后端排序, 前端獲取數(shù)據(jù)渲染)
  7. 排序的過程需要時間, 加一個 loading 效果
  8. 空數(shù)據(jù)效果
  9. 固定表頭
  10. etc....

1. 基本表格

  • 最基本的表格需要表頭,和每一行的數(shù)據(jù), 那我們可不可以只讓用戶傳遞 表頭 props數(shù)據(jù) data, 我們就可以繪制出一個好看的表格, 我們可以參考優(yōu)秀的社區(qū)組件別人是怎么實現(xiàn)的, 向優(yōu)秀的人學(xué)習(xí)
    • 我們可以先定義下面的 columnsdata 的類型以及數(shù)據(jù)結(jié)構(gòu)
type DataPropa = {
  age: number;
  name: string;
  gender: string;
};

type ColProps = {
  title: string;
  key: keyof DataPropa;
};

const columns: ColProps[] = [
  {
    title: "年齡",
    key: "age",
  },
  {
    title: "姓名",
    key: "name",
  },
  {
    title: "性別",
    key: "gender",
  },
];

const data: DataPropa[] = [
  {
    age: 15,
    name: "yym",
    gender: "男",
  },
];

<Table columns={columns} data={data} />
  • 我們在組件實現(xiàn)我們應(yīng)該把這些數(shù)據(jù)渲染到 html 元素上呢? 先完成下面基礎(chǔ)的結(jié)構(gòu)
    • 可以循環(huán) columnsth 元素上, 渲染出我們的表頭
    • 循環(huán) datatbody > tr 有幾個數(shù)據(jù), 就有幾行, 在每個行表格里匹配 columns 里面 key 對應(yīng)的值
import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

interface TableProps<T> {
  columns: {
    title: string;
    key: keyof T;
  }[];
  data: T[];
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const { columns, data } = props;

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table")}>
        <thead className="g-table-head">
          <tr>
            {/* 循環(huán)表頭 */}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item) => {
            return (
              <tr>
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      <span>{item[col.key] as unknown as string}</span>
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
  • 我們給 上面 table 的 class 加上 scss, 美化成我們希望的樣式
.g-table-wrap {
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.06), 0 4px 4px 0 rgba(0, 0, 0, 0.12);

  .g-table {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;

    &-head {
      line-height: 20px;
      tr {
        th {
          padding: 10px;
          font-size: 12px;
          font-weight: 400;
          color: #8e8e93;
        }
      }
    }

    &-body {
      line-height: 20px;
      tr {
        td {
          padding: 13px 10px;
          font-size: 14px;
          color: #575757;
        }
      }
    }

    tr {
      text-align: left;
      border-bottom: 1px solid #f2f2f5;
      &:hover {
        background: #f2faff;
      }
    }
  }
}
普通表格

2. 邊框/緊湊/條紋表格

這些都是樣式的變化,我們來給這些添加對應(yīng)的 class 修改樣式

  • 給每個表格行添加邊框 bordered: boolean
  • 給一個緊湊的表格 compact: boolean
  • 給一個條紋相間的表格 striped: boolean
// 使用
<Table columns={columns} data={data} />
<Table columns={columns} data={data} bordered compact />
// 如何實現(xiàn)
import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

interface TableProps<T> {
  columns: {
    title: string;
    key: keyof T;
  }[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {/* 循環(huán)表頭 */}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item) => {
            return (
              <tr>
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {item[col.key] as unknown as string}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
// 樣式
.g-table-wrap {
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.06), 0 4px 4px 0 rgba(0, 0, 0, 0.12);
  margin-top: 20px;

  .g-table {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;

    &-head {
      line-height: 20px;
      tr {
        th {
          padding: 10px;
          font-size: 12px;
          font-weight: 400;
          color: #8e8e93;
        }
      }
    }

    &-body {
      line-height: 20px;
      tr {
        td {
          padding: 13px 10px;
          font-size: 14px;
          color: #575757;
        }
      }
    }

    tr {
      text-align: left;
      border-bottom: 1px solid #f2f2f5;
      &:hover {
        background: #f2faff;
      }
    }

    &.g-table-bordered {
      border: 1px solid #f2f2f5;
      border-radius: 6px;
      th,
      td {
        border: 1px solid #f2f2f5;
      }
    }
    &.g-table-compact {
      td,
      th {
        padding: 5px;
      }
    }
    &.g-table-striped {
      .g-table-body {
        tr {
          &:nth-child(even) {
            background: #f7f7fa;
          }
          &:hover {
            background: #f2faff;
          }
        }
      }
    }
  }
}
邊框表格

3. 序號列

讓用戶通過配置自動添加序號列, 我們可以設(shè)置一個開關(guān), 來自己添加這一列 numberVisible: boolean

// 如何使用 props numberVisible
<Table columns={columns} data={data} bordered compact numberVisible />
// 實現(xiàn)
import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

interface TableProps<T> {
  columns: {
    title: string;
    key: keyof T;
  }[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {/* 是否顯示序號 */}
            {numberVisible && <th>序號</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {/* 顯示序號的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {item[col.key] as unknown as string}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
image.png

4. 自定義設(shè)置單元格

用戶如果不設(shè)置希望在里面展示的數(shù)據(jù), 我們就自動匹配對應(yīng) columns key 的值, 如果用戶希望展示一個 按鈕 輸入框等, 那我們改怎么弄呢?

  • columns 里面設(shè)置一個回調(diào)函數(shù) render , 讓用戶自定義該列怎么渲染數(shù)據(jù), 我們返回(該單元格內(nèi)容, 整行數(shù)據(jù), 下標(biāo))
  • render 就使用 render 返回渲染, 沒有就使用默認(rèn)的值
// 設(shè)置類型
type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => void;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
}

看一下代碼如何實現(xiàn), 需要改變 columns 數(shù)據(jù), 多了一個參數(shù) render, 正好可以使用到我們之前寫好的 Button 組件

const columns: ColProps[] = [
  {
    title: "年齡",
    key: "age",
  },
  {
    title: "姓名",
    key: "name",
  },
  {
    title: "性別",
    key: "gender",
  },
  {
    title: "地址",
    key: "address",
  },
  {
    title: "操作",
    key: "action",
    render: (text: string, record: DataProps, index: number) => {
      console.log(text, record, index, "data...");
      return <Button type="danger">刪除</Button>;
    },
  },
];

用戶 columns 傳遞了 render, 代碼里我們接收一下

import { ReactElement, ReactNode } from "react";
import classnames from "classnames";

import "./table.scss";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {/* 是否顯示序號 */}
            {numberVisible && <th>序號</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {/* 顯示序號的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的數(shù)據(jù) */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
image.png

5. 設(shè)置選擇框 全選/全不選

一般我們在使用表格組件時都會有選擇數(shù)據(jù)的需求, 所以我們把這個功能集成在 Table 組件里

  • 用戶可以設(shè)置開關(guān)打開選擇框, 可以選擇一個/多個
  • 表頭行可以控制所有行 全選/全不選
  • 選擇時觸發(fā) change 事件把數(shù)據(jù)給吐出去
  • Checkbox 使用我們之前開發(fā)的組件

前面我們設(shè)置了序號列, 我們通過控制 numberVisible 來控制顯隱, 這次我們設(shè)置一個 checkable 來控制選擇框列是否顯隱

// 使用
<Table
  columns={columns}
  data={data}
  bordered
  compact
  numberVisible
  checkable // 控制彈框是否顯示
/>
// 實現(xiàn)
import { ReactElement, ReactNode } from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 選擇框
  checkable?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
    checkable = false,
  } = props;

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {checkable && (
              <th>
                <Checkbox />
              </th>
            )}
            {/* 是否顯示序號 */}
            {numberVisible && <th>序號</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {checkable && (
                  <td>
                    <Checkbox />
                  </td>
                )}
                {/* 顯示序號的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的數(shù)據(jù) */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
選擇框

上面的圖片我們可以看到選擇框已經(jīng)出現(xiàn)在表格列中了, 但是現(xiàn)在有兩個問題沒有解決?

  1. 我們無法從表頭行控制表格數(shù)據(jù)的 全選/全不選
  2. 我們無法得知我們選擇了哪些數(shù)據(jù)

解決1的邏輯就是: 我們可以在組件內(nèi)部當(dāng)表格頭的選擇框被選中時, 給每一行的選擇框 checked = true, 反之 checked = false, 而當(dāng)我們觸發(fā)表格行的選擇框時, 判斷 checked = true 的數(shù)量 等于 data.length 則表頭為選中狀態(tài), 否則就是不完全選擇, 當(dāng)為 0 時, 表頭選擇框為不選, 我們在組件內(nèi)部維護(hù)一個 [selected] = useState([]), 選擇框 change 事件觸發(fā), 根據(jù) selected 的值來判斷 checked

  • 給每個選擇框添加 change 事件
// 實現(xiàn)方案
import { ChangeEvent, ReactElement, ReactNode, useMemo, useState } from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 選擇框
  checkable?: boolean;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,
    checkable = false,
  } = props;

  const [selected, setSelected] = useState<any[]>([]);

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  const handleSelectItem = (e: ChangeEvent<HTMLInputElement>, item: any) => {
    const { checked } = e.target;
    // 改變 checked 的值
    checked
      ? setSelected([...selected, item])
      : setSelected(selected.filter((i) => i.key !== item.key));
  };

  const handleSelectAllItem = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    setSelected(checked ? data : []);
  };

  // 判斷表格行是否被選中
  const areItemSelected = (item: any) =>
    useMemo(
      () => selected.filter((i) => i.key === item.key).length > 0,
      [selected]
    );

  // 表格頭的狀態(tài)
  const areAllItemsSelected: boolean = useMemo(
    () => data.length === selected.length,
    [selected]
  );

  // 不完全選擇
  const areNotAllItemsSelected: boolean = useMemo(
    () => data.length !== selected.length && selected.length !== 0,
    [selected]
  );

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {checkable && (
              <th>
                <Checkbox
                  checked={areAllItemsSelected}
                  indeterminate={areNotAllItemsSelected}
                  onChange={(e) => handleSelectAllItem(e)}
                />
              </th>
            )}
            {/* 是否顯示序號 */}
            {numberVisible && <th>序號</th>}
            {columns.map((col) => {
              return <th key={col.key as string}>{col.title}</th>;
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            console.log(areItemSelected(item), "丁東坑");

            return (
              <tr key={index}>
                {checkable && (
                  <td>
                    <Checkbox
                      checked={areItemSelected(item)}
                      onChange={(e) => handleSelectItem(e, item)}
                    />
                  </td>
                )}
                {/* 顯示序號的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的數(shù)據(jù) */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
狀態(tài)變化

解決2的邏輯就是: 我們可以通過 傳遞一個回調(diào)函數(shù), 把選擇的數(shù)據(jù)給暴露出去, changeSeletedItems props, 在 1 中, 我們創(chuàng)建了 state selected , 我們每次 選擇框改變都會 setSelected 修改值的變化, 我們可以把值傳遞給外部

// 使用
const handleChangeSelected = (val: DataProps[]) => {
  console.log(val, "選中的值");
};

<Table
  columns={columns}
  data={data}
  bordered
  compact
  numberVisible
  checkable
  // 新增 change 事件
  changeSeletedItems={handleChangeSelected}
/>
// 實現(xiàn)方案, 每次 selected 變化時, 把 selected 暴露出去

useEffect(() => {
  changeSeletedItems && changeSeletedItems(selected);
}, [selected]);
選中的值

6. 數(shù)據(jù)排序 sorter

數(shù)據(jù)排序我們可以方法給用戶, 讓用戶來改變 data 的順序, 重新渲染表格來達(dá)到排序的目的, 我們做成下面圖片的樣式, 我們可以在 column 上做文章, 開始是沒有排序, 點擊后會排序 data, 重新渲染

  1. 當(dāng) column 存在排序?qū)傩? 我們就顯示改排序圖標(biāo), 圖標(biāo)變化從 無狀態(tài) => 升序 => 降序
    • 我們可以在內(nèi)部維護(hù)一個 useRef order => "asc" | "desc" | "unsc", 每次點擊規(guī)律變化, 把當(dāng)前狀態(tài)告訴用戶
  2. 用戶使用時在 columns 添加 sorter 函數(shù), 里面調(diào)用后端接口, 改變 data
排序
// 使用
import { Button, Checkbox, CheckboxGroup, Table } from "./lib/index";
import "./App.scss";
import { ChangeEvent, ReactNode, useState } from "react";

type DataProps = {
  age: number;
  name: string;
  gender: string;
  address: string;
  action?: any;
  key?: string;
};
type orderType = "asc" | "desc" | "unsc";

type ColProps<T> = {
  title: string;
  key: keyof DataProps;
  render?: (text: string, record: T, index: number) => ReactNode;
  sorter?: (val: orderType) => void;
};

const App = () => {
  const columns: ColProps<DataProps>[] = [
    {
      title: "年齡",
      key: "age",
      sorter: (val) => {
        // TODO: 開始排序 
        console.log(val, "我是怎么排序規(guī)則?");
        if (val === "asc") {
          data.sort((a, b) => Number(a.age) - Number(b.age));
        } else if (val === "desc") {
          data.sort((a, b) => Number(b.age) - Number(a.age));
        } else {
          // ajax
        }
      },
    },
    {
      title: "姓名",
      key: "name",
    },
    {
      title: "性別",
      key: "gender",
    },
    {
      title: "地址",
      key: "address",
    },
    {
      title: "操作",
      key: "action",
      render: (text: string, record: DataProps, index: number) => {
        return (
          <>
            <Button type="danger" style={{ marginRight: "8px" }}>
              刪除
            </Button>
            <Button type="primary">編輯</Button>
          </>
        );
      },
    },
  ];

  const data: DataProps[] = [
    {
      key: "1",
      age: 15,
      name: "yym",
      gender: "男",
      address: "深圳市",
    },
    {
      key: "2",
      age: 18,
      name: "張三",
      gender: "女",
      address: "安徽省",
    },
    {
      key: "3",
      age: 35,
      name: "李四",
      gender: "女",
      address: "張家界",
    },
    {
      key: "4",
      age: 6,
      name: "小黑",
      gender: "男",
      address: "蚌埠",
    },
  ];

  const handleChangeSelected = (val: DataProps[]) => {
    console.log(val, "選中的值");
  };

  return (
    <div className="App">
      <Table
        columns={columns}
        data={data}
        bordered
        compact
        numberVisible
        checkable
        changeSeletedItems={handleChangeSelected}
      />
    </div>
  );
};

export default App;
// 代碼實現(xiàn)排序
import {
  ChangeEvent,
  ReactElement,
  ReactNode,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type orderType = "asc" | "desc" | "unsc";

type columns<T> = {
  title: string;
  key: keyof T;
  render?: (text: string, record: T, index: number) => ReactNode;
  sorter?: (val: orderType) => void;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 選擇框
  checkable?: boolean;
  changeSeletedItems?: (selected: T[]) => void;
}

const Table: <T>(props: TableProps<T>) => ReactElement = (props) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,

    checkable = false,
    changeSeletedItems,
  } = props;

  const [update, setUpdate] = useState(0); // 更新頁面
  const [selected, setSelected] = useState<any[]>([]);
  const order = useRef<"asc" | "desc" | "unsc">("unsc");

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  useEffect(() => {
    changeSeletedItems && changeSeletedItems(selected);
  }, [selected]);

  const handleSelectItem = (e: ChangeEvent<HTMLInputElement>, item: any) => {
    const { checked } = e.target;
    // 改變 checked 的值
    checked
      ? setSelected([...selected, item])
      : setSelected(selected.filter((i) => i.key !== item.key));
  };

  const handleSelectAllItem = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    setSelected(checked ? data : []);
  };

  // 判斷表格行是否被選中
  const areItemSelected = (item: any) =>
    useMemo(
      () => selected.filter((i) => i.key === item.key).length > 0,
      [selected]
    );

  // 表格頭的狀態(tài)
  const areAllItemsSelected: boolean = useMemo(
    () => data.length === selected.length,
    [selected]
  );

  // 不完全選擇
  const areNotAllItemsSelected: boolean = useMemo(
    () => data.length !== selected.length && selected.length !== 0,
    [selected]
  );

  const handleOrderBy = (col: columns<any>) => {
    if (order.current === "unsc") {
      order.current = "asc";
    } else if (order.current === "asc") {
      order.current = "desc";
    } else if (order.current === "desc") {
      order.current = "unsc";
    }

    setUpdate(Math.random());
    col.sorter && col.sorter(order.current);
  };

  return (
    <div className="g-table-wrap">
      <table className={classnames("g-table", tableClasses)}>
        <thead className="g-table-head">
          <tr>
            {checkable && (
              <th>
                <Checkbox
                  checked={areAllItemsSelected}
                  indeterminate={areNotAllItemsSelected}
                  onChange={(e) => handleSelectAllItem(e)}
                />
              </th>
            )}
            {/* 是否顯示序號 */}
            {numberVisible && <th>序號</th>}
            {columns.map((col) => {
              return (
                <th key={col.key as string}>
                  {/* 排序按鈕 */}
                  {col.sorter ? (
                    <span
                      className="g-table-sort-wrap"
                      onClick={() => handleOrderBy(col)}
                    >
                      {col.title}
                      <span className="g-table-sort">
                        <i
                          className={classnames("g-table-up", {
                            "g-table-active": order.current === "asc",
                          })}
                        ></i>
                        <i
                          className={classnames("g-table-down", {
                            "g-table-active": order.current === "desc",
                          })}
                        ></i>
                      </span>
                    </span>
                  ) : (
                    <>{col.title}</>
                  )}
                </th>
              );
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {checkable && (
                  <td>
                    <Checkbox
                      checked={areItemSelected(item)}
                      onChange={(e) => handleSelectItem(e, item)}
                    />
                  </td>
                )}
                {/* 顯示序號的字段 */}
                {numberVisible && <td>{index + 1}</td>}
                {columns.map((col) => {
                  return (
                    <td key={col.key as string}>
                      {/* 渲染的數(shù)據(jù) */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
原始

asc

desc

7. 添加loading

添加 loading 效果, 就是我們設(shè)置一個 loading 動畫, 用戶傳遞參數(shù) true/false, 展示/隱藏

// 使用
<Table columns={columns} data={data} loading />
// 實現(xiàn)
// html 結(jié)構(gòu) wrap 下和 table 平級
{loading && <div className="g-table-loading">加載中...</div>}
.g-table-loading {
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0.7;
  background: #fff;
}

8. 空數(shù)據(jù)

當(dāng)用戶沒有數(shù)據(jù)時, 我們不能只展示 表頭, 而是展示一個默認(rèn)的樣式, 可以通過 data.length > 0 ? 空數(shù)據(jù)樣式 : tbody, 也可以讓用戶傳進(jìn)來一個 empty 的內(nèi)容, 我們接受放入空數(shù)組頁面里

// 實現(xiàn)方案
{data.length === 0 && (
  <tr>
    <td
      className="g-table-empty"
      colSpan={columns.length + colSpan()}
    >
      <span className="default">暫無數(shù)據(jù)</span>
    </td>
  </tr>
)}
.g-table-empty {
  text-align: center;
  .default {
    display: flex;
    justify-content: center;
    padding: 20px 0;
  }
}

9. 固定表頭

很多情況下當(dāng)我們希望給 table 一個固定高度, 超出滾動時, 我們希望用戶能夠一直看到表頭, 那這個我們應(yīng)該怎么做呢?

  1. 使用 position: sticky 粘性布局, 優(yōu)點是比較簡單, 缺點是兼容性不是很好, can i use position sticky
    • 用戶設(shè)置 height={固定高度} 超過出現(xiàn)滾動條 overflow: auto, 給 thead設(shè)置 position: sticky
// 使用
<Table columns={columns} data={data} height={400} />
// 實現(xiàn)

<div
  className="g-table-wrap"
  style={{ height: height, overflow: height ? "auto" : "unset" }}
>
  <thead
    className={classnames("g-table-head", {
      "g-table-sticky": !!height,
    })}
  >
</div>

&.g-table-sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 0px; /* 列首永遠(yuǎn)固定在頭部  */
    background: #fff;
    z-index: 10;
}

// 固定第一列
&:first-child {
  position: -webkit-sticky;
  position: sticky;
  left: 0;
  background-color: #fff;
}
// 固定第二列
&:nth-child(2) {
  position: -webkit-sticky;
  position: sticky;
  left: 93px; // 讓用戶指定固定列的寬度, 獲取設(shè)置
  background: #fff;
}
固定兩列
  • 一些主流 UI 組件庫因為有很多用戶使用, 做到兼容性比較好, 所以通過 操作 DOM 來把 thead clone 一份, 通過定位放到 table 的上面 來完成表頭固定, 但比較復(fù)雜, 會發(fā)現(xiàn)寬度出現(xiàn)對不齊的情況. 優(yōu)點是兼容性好, 實現(xiàn)起來復(fù)雜
    • 設(shè)置了 height 第一次渲染, 把 tHeadtBody 分開, 讓 tBody overflow: auto
    • 會有 theadtBody 對齊的問題, 簡單的弄, 讓用戶給每個 columns 設(shè)置寬度
    • 下面的實現(xiàn) 當(dāng) 啟用嚴(yán)格模式 時, 會觸發(fā)副作用, 實現(xiàn)原理是這樣的, 暫時先不修復(fù)
// 實現(xiàn)原理: 操作DOM

import {
  ChangeEvent,
  ReactNode,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Checkbox } from "../index";

import classnames from "classnames";

import "./table.scss";

type orderType = "asc" | "desc" | "unsc";

type columns<T> = {
  title: string;
  key: keyof T;
  width?: number;
  render?: (text: string, record: T, index: number) => ReactNode;
  sorter?: (val: orderType) => void;
};

interface TableProps<T> {
  columns: columns<T>[];
  data: T[];
  bordered?: boolean;
  compact?: boolean;
  striped?: boolean;
  numberVisible?: boolean;
  // 選擇框
  checkable?: boolean;
  changeSeletedItems?: (selected: T[]) => void;

  loading?: boolean;
  height?: number;
}
// function Table<T>(props: TableProps<T>)
const Table = <T,>(props: TableProps<T>) => {
  const {
    columns,
    data,
    bordered = false,
    compact = false,
    striped = true,
    numberVisible = false,

    checkable = false,
    changeSeletedItems,

    loading = false,
    height,
  } = props;

  const [_, setUpdate] = useState(0); // 更新頁面
  const [selected, setSelected] = useState<any[]>([]);
  const order = useRef<"asc" | "desc" | "unsc">("unsc");
  const wrapRef = useRef<any>(null);
  const tableRef = useRef<any>(null);

  const tableClasses = {
    "g-table-bordered": bordered,
    "g-table-compact": compact,
    "g-table-striped": striped,
  };

  useEffect(() => {
    changeSeletedItems && changeSeletedItems(selected);
  }, [selected]);

  // 固定表頭計算
  useEffect(() => {
    let table1: any;
    let table2: any;
    if (height) {
      table1 = tableRef.current.cloneNode(false);
      table2 = tableRef.current.cloneNode(false);

      const tHead = tableRef.current.children[0];
      const tBody = tableRef.current.children[1];
      const divBody = document.createElement("div");

      table1.appendChild(tHead);
      divBody.appendChild(table2).appendChild(tBody);
      divBody.style.height = height + "px";
      divBody.style.overflowY = "auto";

      wrapRef.current.appendChild(table1);
      wrapRef.current.appendChild(divBody);
    }

    return () => {
      height && table1.remove();
      height && table2.remove();
    };
  }, []);

  const handleSelectItem = (e: ChangeEvent<HTMLInputElement>, item: any) => {
    const { checked } = e.target;
    // 改變 checked 的值
    checked
      ? setSelected([...selected, item])
      : setSelected(selected.filter((i) => i.key !== item.key));
  };

  const handleSelectAllItem = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    setSelected(checked ? data : []);
  };

  // 判斷表格行是否被選中
  const areItemSelected = (item: T) =>
    useMemo(
      () => selected.filter((i) => i.key === item.key).length > 0,
      [selected]
    );

  // 表格頭的狀態(tài)
  const areAllItemsSelected: boolean = useMemo(
    () => data.length === selected.length,
    [selected]
  );

  // 不完全選擇
  const areNotAllItemsSelected: boolean = useMemo(
    () => data.length !== selected.length && selected.length !== 0,
    [selected]
  );

  const handleOrderBy = (col: columns<T>) => {
    if (order.current === "unsc") {
      order.current = "asc";
    } else if (order.current === "asc") {
      order.current = "desc";
    } else if (order.current === "desc") {
      order.current = "unsc";
    }

    setUpdate(Math.random());

    col.sorter && col.sorter(order.current);
  };

  // 計算 colspan 的 值
  const colSpan = (): number => {
    let length = 0;
    if (numberVisible) {
      length += 1;
    }
    if (checkable) {
      length += 1;
    }

    return length;
  };

  return (
    <div ref={wrapRef} className="g-table-wrap">
      <table ref={tableRef} className={classnames("g-table", tableClasses)}>
        <thead className={classnames("g-table-head")}>
          <tr>
            {checkable && (
              <th style={{ width: "50px" }}>
                <Checkbox
                  checked={areAllItemsSelected}
                  indeterminate={areNotAllItemsSelected}
                  onChange={(e) => handleSelectAllItem(e)}
                />
              </th>
            )}
            {/* 是否顯示序號 */}
            {numberVisible && <th style={{ width: "50px" }}>序號</th>}

            {columns.map((col) => {
              return (
                <th key={col.key as string} style={{ width: `${col.width}px` }}>
                  {/* 排序按鈕 */}
                  {col.sorter ? (
                    <span
                      className="g-table-sort-wrap"
                      onClick={() => handleOrderBy(col)}
                    >
                      {col.title}
                      <span className="g-table-sort">
                        <i
                          className={classnames("g-table-up", {
                            "g-table-active": order.current === "asc",
                          })}
                        ></i>
                        <i
                          className={classnames("g-table-down", {
                            "g-table-active": order.current === "desc",
                          })}
                        ></i>
                      </span>
                    </span>
                  ) : (
                    <>{col.title}</>
                  )}
                </th>
              );
            })}
          </tr>
        </thead>

        <tbody className="g-table-body">
          {data.map((item, index) => {
            return (
              <tr key={index}>
                {checkable && (
                  <td style={{ width: "50px" }}>
                    <Checkbox
                      checked={areItemSelected(item)}
                      onChange={(e) => handleSelectItem(e, item)}
                    />
                  </td>
                )}
                {/* 顯示序號的字段 */}
                {numberVisible && (
                  <td style={{ width: "50px" }}>{index + 1}</td>
                )}
                {columns.map((col) => {
                  return (
                    <td
                      key={col.key as string}
                      style={{ width: `${col.width}px` }}
                    >
                      {/* 渲染的數(shù)據(jù) */}
                      {col.render
                        ? col.render(
                            item[col.key] as unknown as string,
                            item,
                            index
                          )
                        : (item[col.key] as unknown as string)}
                    </td>
                  );
                })}
              </tr>
            );
          })}
          {data.length === 0 && (
            <tr>
              <td
                className="g-table-empty"
                colSpan={columns.length + colSpan()}
              >
                <span className="default">暫無數(shù)據(jù)</span>
              </td>
            </tr>
          )}
        </tbody>
      </table>
      {loading && <div className="g-table-loading">加載中...</div>}
    </div>
  );
};

export default Table;
固定表頭

后續(xù)會增加 固定一列, 展開行等功能

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末澈驼,一起剝皮案震驚了整個濱河市捏雌,隨后出現(xiàn)的幾起案子浇衬,更是在濱河造成了極大的恐慌彬檀,老刑警劉巖姨蝴,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焊傅,死亡現(xiàn)場離奇詭異部蛇,居然都是意外死亡摊唇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門涯鲁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巷查,“玉大人,你說我怎么就攤上這事抹腿〉呵耄” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵警绩,是天一觀的道長崇败。 經(jīng)常有香客問我,道長肩祥,這世上最難降的妖魔是什么后室? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮混狠,結(jié)果婚禮上岸霹,老公的妹妹穿的比我還像新娘。我一直安慰自己将饺,他們只是感情好贡避,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著予弧,像睡著了一般刮吧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掖蛤,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天杀捻,我揣著相機(jī)與錄音,去河邊找鬼贪惹。 笑死,一個胖子當(dāng)著我的面吹牛球榆,可吹牛的內(nèi)容都是我干的肌毅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纯衍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起惶桐,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎潘懊,沒想到半個月后姚糊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡授舟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年救恨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片释树。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡肠槽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奢啥,到底是詐尸還是另有隱情秸仙,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布桩盲,位于F島的核電站寂纪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赌结。R本人自食惡果不足惜捞蛋,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望姑曙。 院中可真熱鬧襟交,春花似錦、人聲如沸伤靠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宴合。三九已至焕梅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卦洽,已是汗流浹背贞言。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留阀蒂,地道東北人该窗。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓弟蚀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酗失。 傳聞我的和親對象是個殘疾皇子义钉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容