Table 组件
可以先看代码, 代码查看
表格组件在我们开发工作中是最常用的整理数据的一种组件, 会在很多页面用到, 我们不可能每个页面都用
table tr td
这些属性写一个表格, 那我们改如何写一个通用的表格组件呢? 我们从一个最基本的表格慢慢完善一个功能强大的表格
我们思考一下如何让用户更好的使用, 我们可以做哪些简单的工作?
- 用户可以使用 基本表格
- 用户可以设置 边框/紧凑/条纹表格
- 用户可以设置序号列, 不需要自己在
columns
里面设置 - 用户可以自定义设置单元格需要展示的数据
- 用户可以设置选择框, 获取表格数据
- 用户可以根据列排序 (后端排序, 前端获取数据渲染)
- 排序的过程需要时间, 加一个 loading 效果
- 空数据效果
- 固定表头
- etc....
1. 基本表格
- 最基本的表格需要表头,和每一行的数据, 那我们可不可以只让用户传递
表头 props
和数据 data
, 我们就可以绘制出一个好看的表格, 我们可以参考优秀的社区组件别人是怎么实现的, 向优秀的人学习- 我们可以先定义下面的
columns
和data
的类型以及数据结构
- 我们可以先定义下面的
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} />
- 我们在组件实现我们应该把这些数据渲染到
html
元素上呢? 先完成下面基础的结构- 可以循环
columns
在th
元素上, 渲染出我们的表头 - 循环
data
在tbody > tr
有几个数据, 就有几行, 在每个行表格里匹配columns
里面key
对应的值
- 可以循环
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>
{/* 循环表头 */}
{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. 边框/紧凑/条纹表格
这些都是样式的变化,我们来给这些添加对应的
class
修改样式
- 给每个表格行添加边框
bordered: boolean
- 给一个紧凑的表格
compact: boolean
- 给一个条纹相间的表格
striped: boolean
// 使用
<Table columns={columns} data={data} />
<Table columns={columns} data={data} bordered compact />
// 如何实现
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>
{/* 循环表头 */}
{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. 序号列
让用户通过配置自动添加序号列, 我们可以设置一个开关, 来自己添加这一列
numberVisible: boolean
// 如何使用 props numberVisible
<Table columns={columns} data={data} bordered compact numberVisible />
// 实现
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. 自定义设置单元格
用户如果不设置希望在里面展示的数据, 我们就自动匹配对应
columns key
的值, 如果用户希望展示一个按钮 输入框等
, 那我们改怎么弄呢?
- 在
columns
里面设置一个回调函数render
, 让用户自定义该列怎么渲染数据, 我们返回(该单元格内容, 整行数据, 下标)
- 有
render
就使用render 返回渲染
, 没有就使用默认的值
// 设置类型
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;
}
看一下代码如何实现, 需要改变 columns 数据
, 多了一个参数 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}>
{/* 渲染的数据 */}
{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. 设置选择框 全选/全不选
一般我们在使用表格组件时都会有选择数据的需求, 所以我们把这个功能集成在
Table 组件里
- 用户可以设置开关打开选择框, 可以选择一个/多个
- 表头行可以控制所有行 全选/全不选
- 选择时触发
change
事件把数据给吐出去 -
Checkbox
使用我们之前开发的组件
前面我们设置了序号列, 我们通过控制 numberVisible
来控制显隐, 这次我们设置一个 checkable
来控制选择框列是否显隐
// 使用
<Table
columns={columns}
data={data}
bordered
compact
numberVisible
checkable // 控制弹框是否显示
/>
// 实现
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}>
{/* 渲染的数据 */}
{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;
上面的图片我们可以看到选择框已经出现在表格列中了, 但是现在有两个问题没有解决?
- 我们无法从表头行控制表格数据的 全选/全不选
- 我们无法得知我们选择了哪些数据
解决1的逻辑就是: 我们可以在组件内部当表格头的选择框被选中时, 给每一行的选择框 checked = true
, 反之 checked = false
, 而当我们触发表格行的选择框时, 判断 checked = true 的数量 等于 data.length
则表头为选中状态, 否则就是不完全选择, 当为 0
时, 表头选择框为不选, 我们在组件内部维护一个 [selected] = useState([])
, 选择框 change
事件触发, 根据 selected
的值来判断 checked
- 给每个选择框添加
change
事件
// 实现方案
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]
);
// 表格头的状态
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}>
{/* 渲染的数据 */}
{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的逻辑就是: 我们可以通过 传递一个回调函数, 把选择的数据给暴露出去, changeSeletedItems props
, 在 1 中, 我们创建了 state selected
, 我们每次 选择框改变都会 setSelected
修改值的变化, 我们可以把值传递给外部
// 使用
const handleChangeSelected = (val: DataProps[]) => {
console.log(val, "选中的值");
};
<Table
columns={columns}
data={data}
bordered
compact
numberVisible
checkable
// 新增 change 事件
changeSeletedItems={handleChangeSelected}
/>
// 实现方案, 每次 selected 变化时, 把 selected 暴露出去
useEffect(() => {
changeSeletedItems && changeSeletedItems(selected);
}, [selected]);
6. 数据排序 sorter
数据排序我们可以方法给用户, 让用户来改变
data
的顺序, 重新渲染表格来达到排序的目的, 我们做成下面图片的样式, 我们可以在column
上做文章, 开始是没有排序, 点击后会排序data
, 重新渲染
- 当
column
存在排序属性, 我们就显示改排序图标, 图标变化从 无状态 => 升序 => 降序- 我们可以在内部维护一个
useRef order => "asc" | "desc" | "unsc"
, 每次点击规律变化, 把当前状态告诉用户
- 我们可以在内部维护一个
- 用户使用时在
columns 添加 sorter 函数
, 里面调用后端接口, 改变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, "我是怎么排序规则?");
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;
// 代码实现排序
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]
);
// 表格头的状态
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}>
{/* 渲染的数据 */}
{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 效果, 就是我们设置一个 loading 动画, 用户传递参数 true/false, 展示/隐藏
// 使用
<Table columns={columns} data={data} loading />
// 实现
// html 结构 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. 空数据
当用户没有数据时, 我们不能只展示 表头, 而是展示一个默认的样式, 可以通过
data.length > 0 ? 空数据样式 : tbody
, 也可以让用户传进来一个empty
的内容, 我们接受放入空数组页面里
// 实现方案
{data.length === 0 && (
<tr>
<td
className="g-table-empty"
colSpan={columns.length + colSpan()}
>
<span className="default">暂无数据</span>
</td>
</tr>
)}
.g-table-empty {
text-align: center;
.default {
display: flex;
justify-content: center;
padding: 20px 0;
}
}
9. 固定表头
很多情况下当我们希望给 table 一个固定高度, 超出滚动时, 我们希望用户能够一直看到表头, 那这个我们应该怎么做呢?
- 使用
position: sticky
粘性布局, 优点是比较简单, 缺点是兼容性不是很好, can i use position sticky- 用户设置
height={固定高度}
超过出现滚动条overflow: auto
, 给thead
设置position: sticky
- 用户设置
// 使用
<Table columns={columns} data={data} height={400} />
// 实现
<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; /* 列首永远固定在头部 */
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; // 让用户指定固定列的宽度, 获取设置
background: #fff;
}
- 一些主流 UI 组件库因为有很多用户使用, 做到兼容性比较好, 所以通过
操作 DOM
来把thead clone 一份, 通过定位放到 table 的上面
来完成表头固定, 但比较复杂, 会发现宽度出现对不齐的情况. 优点是兼容性好, 实现起来复杂- 设置了
height
第一次渲染, 把tHead
和tBody
分开, 让tBody overflow: auto
- 会有
thead
和tBody
对齐的问题, 简单的弄, 让用户给每个columns
设置宽度 - 下面的实现 当
启用严格模式
时, 会触发副作用, 实现原理是这样的, 暂时先不修复
- 设置了
// 实现原理: 操作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]
);
// 表格头的状态
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` }}
>
{/* 渲染的数据 */}
{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">暂无数据</span>
</td>
</tr>
)}
</tbody>
</table>
{loading && <div className="g-table-loading">加载中...</div>}
</div>
);
};
export default Table;
后续会增加 固定一列, 展开行等功能