@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
1,609 lines (1,463 loc) • 230 kB
Markdown
---
localeCode: zh-CN
order: 81
category: 展示类
title: Table 表格
icon: doc-table
brief: 表格用于呈现结构化的数据内容,通常会伴随提供对数据进行操作(排序、搜索、分页……)的能力。
---
## 如何使用
往 Table 传入表头 `columns` 和数据 `dataSource` 进行渲染。
<Notice title='注意事项'>
请为 `dataSource` 中的每个数据项提供一个与其他数据项值不同的 `key`,或者使用 `rowKey` 参数指定一个作为主键的属性名,表格的行选择、展开等绝大多数行操作功能都会使用到。
</Notice>
```jsx import
import React from 'react';
import { Table, Tag } from '@douyinfe/semi-ui';
function App() {
const columns = [
{
title: '标题',
dataIndex: 'name',
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
];
const data = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
return <Table columns={columns} dataSource={data} pagination={false} />;
}
```
## 代码演示
### 基本表格
对于表格,最基本的两个参数为 `dataSource` 和 `columns`,前者为数据项,后者为每列的配置,二者皆为数组类型。
```jsx live=true noInline=true dir="column"
import React from 'react';
import { Table, Avatar, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear } from '@douyinfe/semi-icons';
function App() {
const columns = [
{
title: '标题',
dataIndex: 'name',
render: (text, record, index) => {
return (
<div>
<Avatar
size="small"
shape="square"
src={record.nameIconSrc}
style={{ marginRight: 12 }}
></Avatar>
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text];
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
},
},
];
const data = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
return <Table columns={columns} dataSource={data} pagination={false} />;
}
render(App);
```
### JSX 写法
你也可以使用 JSX 语法定义 `columns`,注意 Table 仅支持 `columns` 的 JSX 语法定义。你不能够使用任何组件包裹 `Table.Column` 组件。
<Notice type="primary" title="注意事项">
<div>1. JSX 写法的表格暂时不支持 resizable 功能;</div>
<div>2. 使用 JSX 写法时,请不要与配置写法同时使用;如果同时使用,仅配置写法生效,不会进行聚合操作。</div>
</Notice>
```jsx live=true noInline=true dir="column"
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
const { Column } = Table;
function App() {
const data = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const renderName = (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
};
const renderOwner = (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
return (
<Table dataSource={data} pagination={false}>
<Column title="标题" dataIndex="name" key="name" render={renderName} />
<Column title="大小" dataIndex="size" key="size" />
<Column title="所有者" dataIndex="owner" key="owner" render={renderOwner} />
<Column title="更新时间" dataIndex="updateTime" key="updateTime" />
<Column title="" dataIndex="operate" key="operate" render={() => <IconMore />} />
</Table>
);
}
render(App);
```
### 行选择操作
往 Table 传入 [rowSelection](#rowSelection) 即可打开此功能。
- 点击表头的选择框,会选择 `dataSource` 里所有不是 `disabled` 状态的行。选择所有行回调函数为 `onSelectAll`;
- 点击行的选择框会选中当前行。它的回调函数为 `onSelect`;
<Notice title='注意事项'>
1. 请务必为 `dataSource` 中每行数据提供一个与其他行值不同的 `key`,或者使用 `rowKey` 参数指定一个作为主键的属性名。
2. 如你遇见在第二页点击行选择后,回到第一页问题,请检查组件渲染是否触发了 `dataSource` 更新(浅对比)。`dataSource` 更新后,非受控的翻页器会回到第一页。请将 `dataSource` 放在 state 内。
</Notice>
```jsx live=true noInline=true dir="column"
import React from 'react';
import { Table, Avatar, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear } from '@douyinfe/semi-icons';
function App() {
const [selectedKeys, setSelectedKeys] = useState([]);
const columns = useMemo(() => [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar
size="small"
shape="square"
src={record.nameIconSrc}
style={{ marginRight: 12 }}
></Avatar>
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text];
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
},
},
], []);
const data = useMemo(() => [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计稿.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
status: 'wait',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi D2C 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: 'Semi D2C 设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'success',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
], []);
const rowSelection = {
getCheckboxProps: record => ({
disabled: record.name === '设计文档', // Column configuration not to be checked
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
setSelectedKeys(selectedRowKeys);
},
};
const pagination = useMemo(
() => ({
pageSize: 3,
}),
[]
);
return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={pagination} />;
}
render(App);
```
### 自定义渲染
用户可以使用 `Column.render` 来自定义某一列单元格的渲染,该功能适用于需要渲染较为复杂的单元格内容时。
`render` 函数的第四个参数 `options` 是一个对象,包含以下属性:
- `expandIcon`: 展开图标(当使用树形数据或可展开行时)
- `selection`: 选择框(当开启行选择时)
- `indentText`: 缩进内容(当使用树形数据时)
- `isHovering`: 当前行是否处于悬停状态(v2.98.0 支持)
通过 `isHovering` 参数,可以实现鼠标悬停时显示操作按钮等交互效果。
```jsx live=true noInline=true dir="column"
import React from 'react';
import { Table, Avatar, Button, Empty, Typography, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear, IconDelete } from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
const { Text } = Typography;
const raw = [
{
key: '1',
name: 'Semi Design 设计稿标题可能有点长这时候应该显示 Tooltip.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计文档可能也有点长所以也会显示 Tooltip',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
status: 'success',
owner: '姜琪',
updateTime: '2020-01-26 11:01',
avatarBg: 'green',
},
];
function App() {
const [dataSource, setData] = useState(raw);
const removeRecord = key => {
let newDataSource = [...dataSource];
if (key != null) {
let idx = newDataSource.findIndex(data => data.key === key);
if (idx > -1) {
newDataSource.splice(idx, 1);
setData(newDataSource);
}
}
};
const resetData = () => {
const newDataSource = [...raw];
setData(newDataSource);
};
const columns = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<span style={{ display: 'flex', alignItems: 'center' }}>
<Avatar
size="small"
shape="square"
src={record.nameIconSrc}
style={{ marginRight: 12 }}
></Avatar>
{/* 宽度计算方式为单元格设置宽度 - 非文本内容宽度 */}
<Text ellipsis={{ showTooltip: true }} style={{ width: 'calc(400px - 76px)' }}>
{text}
</Text>
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
width: 150,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text];
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
width: 300,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
},
{
title: '',
dataIndex: 'operate',
render: (text, record) => (
<Button icon={<IconDelete />} theme="borderless" onClick={() => removeRecord(record.key)} />
),
},
];
const empty = (
<Empty
image={<IllustrationNoResult />}
darkModeImage={<IllustrationNoResultDark />}
description={'搜索无结果'}
/>
);
return (
<>
<Button onClick={resetData} style={{ marginBottom: 10 }}>
重置
</Button>
<Table
style={{ minHeight: 350 }}
columns={columns}
dataSource={dataSource}
pagination={false}
empty={empty}
/>
</>
);
}
render(App);
```
### 带分页组件的表格
表格分页目前支持两种模式:受控和非受控模式。
- 受控模式下,分页的状态完全由外部传入,依据为是否往 Table 传入了 `pagination.currentPage` 这个字段。一般情况下,受控模式适用于远程拉取数据并渲染。
- 非受控模式下,Table 默认会将传入的 `dataSource` 长度作为 `total` 传给 Pagination 组件,当然你也可以传入一个 `total` 字段来覆盖 Table 组件的取值,不过我们并不推荐用户在非受控分页模式下传入这个字段。
```jsx live=true noInline=true dir="column"
import React, { useState, useMemo } from 'react';
import { Table, Avatar, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear, IconDelete } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: text => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text] || {};
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const DAY = 24 * 60 * 60 * 1000;
function App() {
const [dataSource, setData] = useState([]);
const rowSelection = useMemo(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
}),
[]
);
const scroll = useMemo(() => ({ y: 300 }), []);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
```
### 拉取远程数据
正常情况下,数据往往不是一次性获取的,我们会在点击页码、过滤器或者排序按钮时从接口重新获取数据,这种情况下请使用**受控模式**来处理分页。用户需往 Table 传入 `pagination.currentPage` 这个字段,此时分页组件的渲染完全依赖于传入的 `pagination` 对象。
<Notice type="primary" title="注意事项">
<div>1. 非受控时,pagination 如果是对象类型则不推荐使用字面量写法,原因是字面量写法会导致表格渲染至初始状态(看起来像是分页器没有生效)。请尽量将引用型参数定义在 render 方法之外,如果使用了 hooks 请利用 useMemo 或 useState 进行存储;</div>
<div>2. 受控模式下,Table 不会对 dataSource 分页,请给 dataSource 传入当前页数据</div>
</Notice>
```jsx live=true noInline=true dir="column"
import React, { useState, useMemo } from 'react';
import { Table, Avatar, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear, IconDelete } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const pageSize = 5;
const columns = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: text => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text] || {};
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
function App() {
const [dataSource, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [currentPage, setPage] = useState(1);
const fetchData = (currentPage = 1) => {
setLoading(true);
setPage(currentPage);
return new Promise((res, rej) => {
setTimeout(() => {
const data = getData();
let dataSource = data.slice((currentPage - 1) * pageSize, currentPage * pageSize);
res(dataSource);
}, 300);
}).then(dataSource => {
setLoading(false);
setData(dataSource);
});
};
const handlePageChange = page => {
fetchData(page);
};
useEffect(() => {
fetchData();
}, []);
return (
<Table
columns={columns}
dataSource={dataSource}
pagination={{
currentPage,
pageSize: 5,
total: data.length,
onPageChange: handlePageChange,
}}
loading={loading}
/>
);
}
render(App);
```
### 固定列或表头
可以通过设置 column 的 `fixed` 属性以及 `scroll.x` 来进行列固定,通过设置 `scroll.y` 来进行表头固定。
如果是固定值,设置为 >=所有固定列宽之和 + 所有表格列宽之和 的数值。
> - 建议指定 `scroll.x` 为大于表格宽度的**固定值**或百分比。如果是固定值,设置为 `>=所有固定列宽之和+所有表格列宽之和` 的数值。
> - 若列头与内容不对齐或出现列重复或者固定列失效的情况,请指定固定列的宽度 `width`,若指定宽度后仍不生效,请尝试建议留一列不设宽度以适应弹性布局,或者检查是否有超长连续字段破坏布局。
> - 请确保表格内部的所有元素在渲染后不会对单元格的高度造成影响(例如含有未加载完成的图片等),这种情况下请给定子元素一个确定的高度,以此确保左右固定列单元格不会错乱。
```jsx live=true noInline=true dir="column"
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 200,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: text => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => {
return <IconMore />;
},
},
];
function App() {
const [dataSource, setData] = useState([]);
const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
const rowSelection = useMemo(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
fixed: true,
}),
[]
);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
```
通过 `sticky` 属性可以将表头固定在页面顶部。v2.21 版本支持。传入 `top` 时可以控制距离滚动容器的距离。
开启 sticky 后,Table 会自动打开 fixed 布局,列宽将由 `column.width` 决定。没有给定 width 的列宽由浏览器自动分配。
<StickyHeaderTable />
```jsx live=false noInline=true dir="column"
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 200,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: text => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => {
return <IconMore />;
},
},
];
function App() {
const [dataSource, setData] = useState([]);
const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
const rowSelection = useMemo(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
fixed: true,
}),
[]
);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return (
<Table
sticky={{ top: 60 }}
columns={columns}
dataSource={dataSource}
rowSelection={rowSelection}
scroll={scroll}
/>
);
}
render(App);
```
### 带排序和过滤功能的表头
表格内部集成了过滤器和排序控件,用户可以通过在 Column 中传入 `filters` 以及 `onFilter` 开启表头的过滤器控件展示,传入 `sorter` 开启表头的排序控件的展示。
<Notice title='注意事项'>
1. 请为 `dataSource` 中的每个数据项提供一个与其他数据项值不同的 `key`,或者使用 `rowKey` 参数指定一个作为主键的属性名,表格的行选择、展开等绝大多数行操作功能都会使用到。
2. 排序和筛选列必须设置独立的 `dataIndex`
</Notice>
```jsx live=true noInline=true dir="column"
import React, { useState, useMemo } from 'react';
import { Table, Avatar, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear, IconDelete } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: text => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text];
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
function App() {
const [dataSource, setData] = useState([]);
const rowSelection = useMemo(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
}),
[]
);
const scroll = useMemo(() => ({ y: 300 }), []);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
```
sorter 为函数类型时,可以通过函数的第三个参数获取 sortOrder 状态。函数类型为 `(a?: RecordType, b?: RecordType, sortOrder?: 'ascend' | 'descend') => number`。v2.47 版本支持。
可通过 `showSortTip` 属性控制是否展示排序提示,自 v2.65 版本支持,默认为 `false`。当开启提示后,当仅有排序功能时候,鼠标移动至表头时,会展示排序提示;其他情况下,仅鼠标移动至排序图标时,会展示排序提示。
**注**:在使用 `sortOrder` 属性受控排序时,由于无法预测下一个排序顺序,因此 `showSortTip` 不生效,不会展示提示。
```jsx live=true noInline=true dir="column"
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
function App() {
const columns = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
}
},
{
title: '大小',
dataIndex: 'size',
sorter: (r1, r2, order) => {
const a = r1.size;
const b = r2.size;
if (typeof a === "number" && typeof b === "number") {
return a - b; // 数字正常比较大小
} else if (typeof a === "undefined") {
return order === "ascend" ? 1 : -1; // undefined 在后面
} else if (typeof b === "undefined") {
return order === "ascend" ? -1 : 1; // undefined 在后面
} else {
return 0; // 保持原来的顺序
}
},
showSortTip: true,
render: text => text ? `${text} KB` : '未知',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const docIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png';
const dataSource = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: figmaIconUrl,
size: 3,
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: docIconUrl,
size: undefined,
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档 3',
nameIconSrc: docIconUrl,
size: 1,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: '设计文档 4',
nameIconSrc: docIconUrl,
size: 5,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '5',
name: '设计文档 5',
nameIconSrc: docIconUrl,
size: undefined,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '6',
name: '设计文档 6',
nameIconSrc: docIconUrl,
size: 2,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
return <Table columns={columns} dataSource={dataSource} />;
}
render(App);
```
### 自定义表头筛选
如果你需要将筛选器输入框展示在表头,可在 `title` 传入 ReactNode,配合 `filteredValue` 使用。
```jsx live=true noInline=true dir="column"
import React, { useState, useEffect, useRef } from 'react';
import { Table, Avatar, Input, Space, Tag } from '@douyinfe/semi-ui';
import { IconMore, IconTickCircle, IconComment, IconClear, IconDelete } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
function App() {
const [dataSource, setData] = useState([]);
const [filteredValue, setFilteredValue] = useState([]);
const compositionRef = useRef({ isComposition: false });
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const handleChange = (value) => {
if (compositionRef.current.isComposition) {
return;
}
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const handleCompositionStart = () => {
compositionRef.current.isComposition = true;
};
const handleCompositionEnd = (event) => {
compositionRef.current.isComposition = false;
const value = event.target.value;
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const columns = [
{
title: (
<Space>
<span>标题</span>
<Input
placeholder="请输入筛选值"
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
</Space>
),
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
onFilter: (value, record) => record.name.includes(value),
filteredValue,
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: text => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
};
const tagProps = tagConfig[text];
return <Tag shape='circle' {...tagProps} style={{ userSelect: 'text' }}>{tagProps.text}</Tag>;
}
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: value => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} />;
}
render(App);
```
### 自定义筛选器
使用 `renderFilterDropdown` 自定义渲染筛选器面板。v2.52 支持。
你可以在用户输入筛选值的时候调用 `setTempFilteredValue` 存储筛选值,在筛选值输入完毕后调用 `confirm` 触发真正的筛选。也可以通过 `confirm({ filteredValue })` 直接筛选。
设置 `tempFilteredValue` 的原因是在需要存储临时筛选值的场景,不需要自己声明一个 state 保存这个临时筛选值。
```typescript
type RenderFilterDropdown = (props: RenderFilterDropdownProps) => React.ReactNode;
interface RenderFilterDropdownProps {
/** 临时筛选值,初始值为 `filteredValue` 或 `defaultFilteredValue` */
tempFilteredValue: any[];
/** 设置临时筛选值 */
setTempFilteredValue: (tempFilteredValue: any[]) => void;
/** `confirm` 默认会将 `tempFilteredValue` 赋值给 `filteredValue` 并触发 `onChange` 事件。你也可以通过传入 `filt