Table 組件
可以先看代碼, 代碼查看
表格組件在我們開發(fā)工作中是最常用的整理數(shù)據(jù)的一種組件, 會在很多頁面用到, 我們不可能每個頁面都用
table tr td
這些屬性寫一個表格, 那我們改如何寫一個通用的表格組件呢? 我們從一個最基本的表格慢慢完善一個功能強(qiáng)大的表格
我們思考一下如何讓用戶更好的使用, 我們可以做哪些簡單的工作?
- 用戶可以使用 基本表格
- 用戶可以設(shè)置 邊框/緊湊/條紋表格
- 用戶可以設(shè)置序號列, 不需要自己在
columns
里面設(shè)置 - 用戶可以自定義設(shè)置單元格需要展示的數(shù)據(jù)
- 用戶可以設(shè)置選擇框, 獲取表格數(shù)據(jù)
- 用戶可以根據(jù)列排序 (后端排序, 前端獲取數(shù)據(jù)渲染)
- 排序的過程需要時間, 加一個 loading 效果
- 空數(shù)據(jù)效果
- 固定表頭
- etc....
1. 基本表格
- 最基本的表格需要表頭,和每一行的數(shù)據(jù), 那我們可不可以只讓用戶傳遞
表頭 props
和數(shù)據(jù) data
, 我們就可以繪制出一個好看的表格, 我們可以參考優(yōu)秀的社區(qū)組件別人是怎么實現(xiàn)的, 向優(yōu)秀的人學(xué)習(xí)- 我們可以先定義下面的
columns
和data
的類型以及數(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)
columns
在th
元素上, 渲染出我們的表頭 - 循環(huán)
data
在tbody > tr
有幾個數(shù)據(jù), 就有幾行, 在每個行表格里匹配columns
里面key
對應(yīng)的值
- 可以循環(huán)
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;
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;
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)在有兩個問題沒有解決?
- 我們無法從表頭行控制表格數(shù)據(jù)的 全選/全不選
- 我們無法得知我們選擇了哪些數(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;
解決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
, 重新渲染
- 當(dāng)
column
存在排序?qū)傩? 我們就顯示改排序圖標(biāo), 圖標(biāo)變化從 無狀態(tài) => 升序 => 降序- 我們可以在內(nèi)部維護(hù)一個
useRef order => "asc" | "desc" | "unsc"
, 每次點擊規(guī)律變化, 把當(dāng)前狀態(tài)告訴用戶
- 我們可以在內(nèi)部維護(hù)一個
- 用戶使用時在
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;
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)該怎么做呢?
- 使用
position: sticky
粘性布局, 優(yōu)點是比較簡單, 缺點是兼容性不是很好, can i use position sticky- 用戶設(shè)置
height={固定高度}
超過出現(xiàn)滾動條overflow: auto
, 給thead
設(shè)置position: sticky
- 用戶設(shè)置
// 使用
<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
第一次渲染, 把tHead
和tBody
分開, 讓tBody overflow: auto
- 會有
thead
和tBody
對齊的問題, 簡單的弄, 讓用戶給每個columns
設(shè)置寬度 - 下面的實現(xiàn) 當(dāng)
啟用嚴(yán)格模式
時, 會觸發(fā)副作用, 實現(xiàn)原理是這樣的, 暫時先不修復(fù)
- 設(shè)置了
// 實現(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ù)會增加 固定一列, 展開行等功能