UNPKG

@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
--- 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