如何使用
往 Table 传入表头 columns 和数据 dataSource 进行渲染。
import React from 'react';
import { Table, Tag } from '@fibrous/semi-ui-react';
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: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/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,前者为数据项,后者为每列的配置,二者皆为数组类型。
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| 2M | 已交付 | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 已延期 | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | 待评审 | ZZoey Edwards | 2020-01-26 11:01 |
代码
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-react';
export default function TableDemo() {
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: 'success' | 'pending' | 'wait') => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/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} />;
}JSX 写法
你也可以使用 JSX 语法定义 columns,注意 Table 仅支持 columns 的 JSX 语法定义。你不能够使用任何组件包裹 Table.Column 组件。
| 标题 | 大小 | 所有者 | 更新时间 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import type { ComponentProps } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar } from '@fibrous/semi-ui-react';
const { Column } = Table;
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: string;
updateTime: string;
avatarBg: ComponentProps<typeof Avatar>['color'];
}
export default function TableDemo() {
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const renderName = (text: string, record: Row) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
};
const renderOwner = (text: string, record: Row) => {
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>
);
}行选择操作
往 Table 传入 rowSelection 即可打开此功能。
- 点击表头的选择框,会选择
dataSource里所有不是disabled状态的行。选择所有行回调函数为onSelectAll; - 点击行的选择框会选中当前行。它的回调函数为
onSelect;
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|---|
| 2M | 已交付 | 姜姜鹏志 | 2020-02-02 05:13 | |||
| 2M | 已延期 | 郝郝宣 | 2020-01-17 05:31 | |||
| 34KB | 待评审 | ZZoey Edwards | 2020-01-26 11:01 |
代码
import { useMemo } from 'react';
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
export default function TableDemo() {
const columns = useMemo<ColumnProps<Row>[]>(
() => [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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<Row[]>(
() => [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/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: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'wait',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi D2C 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: 'Semi D2C 设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'success',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
],
[]
);
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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);
},
};
const pagination = useMemo(
() => ({
pageSize: 3,
}),
[]
);
return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={pagination} />;
}自定义渲染
用户可以使用 Column.render 来自定义某一列单元格的渲染,该功能适用于需要渲染较为复杂的单元格内容时。
render 函数的第四个参数 options 是一个对象,包含以下属性:
expandIcon: 展开图标(当使用树形数据或可展开行时)selection: 选择框(当开启行选择时)indentText: 缩进内容(当使用树形数据时)isHovering: 当前行是否处于悬停状态
通过 isHovering 参数,可以实现鼠标悬停时显示操作按钮等交互效果。
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| 2M | 已交付 | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 已延期 | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | 待评审 | ZZoey Edwards | 2020-01-26 11:01 | ||
| 34KB | 已交付 | 姜姜琪 | 2020-01-26 11:01 |
代码
import { useState } from 'react';
import { IconTickCircle, IconComment, IconClear, IconDelete } from '@fibrous/semi-icons-react';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrous/semi-illustrations-react';
import { Table, Avatar, Button, Empty, Typography, Tag, type ColumnProps } from '@fibrous/semi-ui-react';
const { Text } = Typography;
const raw = [
{
key: '1',
name: 'Semi Design 设计稿标题可能有点长这时候应该显示 Tooltip.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/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: '/assets/docs-icon.png',
size: '34KB',
status: 'success',
owner: '姜琪',
updateTime: '2020-01-26 11:01',
avatarBg: 'green',
},
];
export default function TableDemo() {
const [dataSource, setData] = useState(raw);
const removeRecord = (key: string) => {
let newDataSource = [...dataSource];
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: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<span style={{ display: 'flex', alignItems: 'center' }}>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{/* 宽度 = 单元格设定宽度 − 非文本内容宽度 */}
<Text ellipsis={{ showTooltip: true }} style={{ width: 'calc(400px - 76px)' }}>
{text}
</Text>
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
width: 150,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: 'success' | 'pending' | 'wait') => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
width: 300,
render: (text, record) => {
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 aria-label="删除" 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} />
</>
);
}带分页组件的表格
表格分页目前支持两种模式:受控和非受控模式。
- 受控模式下,分页的状态完全由外部传入,依据为是否往 Table 传入了
pagination.currentPage这个字段。一般情况下,受控模式适用于远程拉取数据并渲染。 - 非受控模式下,Table 默认会将传入的
dataSource长度作为total传给 Pagination 组件,当然你也可以传入一个total字段来覆盖 Table 组件的取值,不过我们并不推荐用户在非受控分页模式下传入这个字段。
标题 | 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2019-07-23 | ||
| 5 KB | 待评审 | 郝郝宣 | 2019-07-28 | ||
| 10 KB | 已交付 | 姜姜鹏志 | 2019-08-02 | ||
| 15 KB | 待评审 | 郝郝宣 | 2019-08-07 | ||
| 20 KB | 已交付 | 姜姜鹏志 | 2019-08-12 | ||
| 25 KB | 待评审 | 郝郝宣 | 2019-08-17 | ||
| 30 KB | 已交付 | 姜姜鹏志 | 2019-08-22 | ||
| 35 KB | 待评审 | 郝郝宣 | 2019-08-27 | ||
| 40 KB | 已交付 | 姜姜鹏志 | 2019-09-01 | ||
| 45 KB | 待评审 | 郝郝宣 | 2019-09-06 |
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: 'success' | 'pending' | 'wait') => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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;
const getData = () => {
const data: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const rowSelection = useMemo(
(): RowSelectionProps<Row> => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
}),
[]
);
const scroll = useMemo(() => ({ y: 300 }), []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}拉取远程数据
正常情况下,数据往往不是一次性获取的,我们会在点击页码、过滤器或者排序按钮时从接口重新获取数据,这种情况下请使用受控模式来处理分页。用户需往 Table 传入 pagination.currentPage 这个字段,此时分页组件的渲染完全依赖于传入的 pagination 对象。
标题 | 交付状态 | 所有者 |
|---|
代码
import { useState, useEffect } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pageSize = 5;
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: 'success' | 'pending' | 'wait') => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
const fetchData = (currentPage: number) => {
return new Promise<Row[]>((res) => {
setTimeout(() => {
res(data.slice((currentPage - 1) * pageSize, currentPage * pageSize));
}, 300);
});
};
export default function TableDemo() {
const [dataSource, setData] = useState<Row[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setPage] = useState(1);
const handlePageChange = (page: number) => {
setLoading(true);
setPage(page);
};
useEffect(() => {
// Drop responses for pages the user has left
let ignore = false;
void fetchData(currentPage).then((dataSource) => {
if (!ignore) {
setLoading(false);
setData(dataSource);
}
});
return () => {
ignore = true;
};
}, [currentPage]);
return (
<Table
columns={columns}
dataSource={dataSource}
pagination={{
currentPage,
pageSize: 5,
total: data.length,
onPageChange: handlePageChange,
}}
loading={loading}
/>
);
}固定列或表头
可以通过设置 column 的 fixed 属性以及 scroll.x 来进行列固定,通过设置 scroll.y 来进行表头固定。
如果是固定值,设置为 >=所有固定列宽之和 + 所有表格列宽之和 的数值。
- 建议指定
scroll.x为大于表格宽度的固定值或百分比。如果是固定值,设置为>=所有固定列宽之和+所有表格列宽之和的数值。- 若列头与内容不对齐或出现列重复或者固定列失效的情况,请指定固定列的宽度
width,若指定宽度后仍不生效,请尝试建议留一列不设宽度以适应弹性布局,或者检查是否有超长连续字段破坏布局。- 请确保表格内部的所有元素在渲染后不会对单元格的高度造成影响(例如含有未加载完成的图片等),这种情况下请给定子元素一个确定的高度,以此确保左右固定列单元格不会错乱。
标题 | 所有者 |
|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | |||
| 5 KB | 郝郝宣 | 2019-07-28 | |||
| 10 KB | 姜姜鹏志 | 2019-08-02 | |||
| 15 KB | 郝郝宣 | 2019-08-07 | |||
| 20 KB | 姜姜鹏志 | 2019-08-12 | |||
| 25 KB | 郝郝宣 | 2019-08-17 | |||
| 30 KB | 姜姜鹏志 | 2019-08-22 | |||
| 35 KB | 郝郝宣 | 2019-08-27 | |||
| 40 KB | 姜姜鹏志 | 2019-09-01 | |||
| 45 KB | 郝郝宣 | 2019-09-06 |
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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) => {
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 />;
},
},
];
const getData = () => {
const data: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
const rowSelection = useMemo<RowSelectionProps<Row>>(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
fixed: true,
}),
[]
);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}通过 sticky 属性可以将表头固定在页面顶部。传入 top 时可以控制距离滚动容器的距离。
开启 sticky 后,Table 会自动打开 fixed 布局,列宽将由 column.width 决定。没有给定 width 的列宽由浏览器自动分配。
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@fibrous/semi-ui-react';
import { IconMore } from '@fibrous/semi-icons-react';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
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', // 这一行不可勾选
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 开启表头的排序控件的展示。
| 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2019-07-23 | ||
| 5 KB | 待评审 | 郝郝宣 | 2019-07-28 | ||
| 10 KB | 已交付 | 姜姜鹏志 | 2019-08-02 | ||
| 15 KB | 待评审 | 郝郝宣 | 2019-08-07 | ||
| 20 KB | 已交付 | 姜姜鹏志 | 2019-08-12 | ||
| 25 KB | 待评审 | 郝郝宣 | 2019-08-17 | ||
| 30 KB | 已交付 | 姜姜鹏志 | 2019-08-22 | ||
| 35 KB | 待评审 | 郝郝宣 | 2019-08-27 | ||
| 40 KB | 已交付 | 姜姜鹏志 | 2019-09-01 | ||
| 45 KB | 待评审 | 郝郝宣 | 2019-09-06 |
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: 'success' | 'pending' | 'wait') => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const rowSelection = useMemo(
(): RowSelectionProps<Row> => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
}),
[]
);
const scroll = useMemo(() => ({ y: 300 }), []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}sorter 为函数类型时,可以通过函数的第三个参数获取 sortOrder 状态。函数类型为 (a?: RecordType, b?: RecordType, sortOrder?: 'ascend' | 'descend') => number。
可通过 showSortTip 属性控制是否展示排序提示,默认为 false。当开启提示后,当仅有排序功能时候,鼠标移动至表头时,会展示排序提示;其他情况下,仅鼠标移动至排序图标时,会展示排序提示。
注:在使用 sortOrder 属性受控排序时,由于无法预测下一个排序顺序,因此 showSortTip 不生效,不会展示提示。
| 标题 | 所有者 | 更新日期 | |
|---|---|---|---|
| 3 KB | 姜姜鹏志 | 2020-02-02 | |
| 未知 | 郝郝宣 | 2020-01-17 | |
| 1 KB | ZZoey Edwards | 2020-01-26 | |
| 5 KB | ZZoey Edwards | 2020-01-26 | |
| 未知 | ZZoey Edwards | 2020-01-26 | |
| 2 KB | ZZoey Edwards | 2020-01-26 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: number | undefined;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
export default function TableDemo() {
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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) => {
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 = '/assets/placeholder/app.svg';
const docIconUrl = '/assets/docs-icon.png';
const dataSource: Row[] = [
{
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} />;
}自定义表头筛选
如果你需要将筛选器输入框展示在表头,可在 title 传入 ReactNode,配合 filteredValue 使用。
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-01-25 | |
| 5 KB | 待评审 | 郝郝宣 | 2024-01-30 | |
| 10 KB | 已交付 | 姜姜鹏志 | 2024-02-04 | |
| 15 KB | 待评审 | 郝郝宣 | 2024-02-09 | |
| 20 KB | 已交付 | 姜姜鹏志 | 2024-02-14 | |
| 25 KB | 待评审 | 郝郝宣 | 2024-02-19 | |
| 30 KB | 已交付 | 姜姜鹏志 | 2024-02-24 | |
| 35 KB | 待评审 | 郝郝宣 | 2024-02-29 | |
| 40 KB | 已交付 | 姜姜鹏志 | 2024-03-05 | |
| 45 KB | 待评审 | 郝郝宣 | 2024-03-10 |
代码
import { useState, useRef, type CompositionEvent } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Input, Space, Tag, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
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;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const [filteredValue, setFilteredValue] = useState<string[]>([]);
const compositionRef = useRef({ isComposition: false });
const figmaIconUrl = '/assets/placeholder/app.svg';
const handleChange = (value: string) => {
if (compositionRef.current.isComposition) {
return;
}
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const handleCompositionStart = () => {
compositionRef.current.isComposition = true;
};
const handleCompositionEnd = (event: CompositionEvent<HTMLInputElement>) => {
compositionRef.current.isComposition = false;
const value = (event.target as HTMLInputElement).value;
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const columns: ColumnProps[] = [
{
title: (
<Space>
<span>标题</span>
<Input placeholder="请输入筛选值" style={{ width: 200 }} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onChange={handleChange} showClear />
</Space>
),
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: 'success' | 'pending' | 'wait') => {
const tagConfig = {
success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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');
},
},
];
return <Table columns={columns} dataSource={dataSource} />;
}自定义筛选器
使用 renderFilterDropdown 自定义渲染筛选器面板。
你可以在用户输入筛选值的时候调用 setTempFilteredValue 存储筛选值,在筛选值输入完毕后调用 confirm 触发真正的筛选。也可以通过 confirm({ filteredValue }) 直接筛选。
设置 tempFilteredValue 的原因是在需要存储临时筛选值的场景,不需要自己声明一个 state 保存这个临时筛选值。
type RenderFilterDropdown = (props: RenderFilterDropdownProps) => React.ReactNode;
interface RenderFilterDropdownProps {
/** 临时筛选值,初始值为 `filteredValue` 或 `defaultFilteredValue` */
tempFilteredValue: any[];
/** 设置临时筛选值 */
setTempFilteredValue: (tempFilteredValue: any[]) => void;
/** `confirm` 默认会将 `tempFilteredValue` 赋值给 `filteredValue` 并触发 `onChange` 事件。你也可以通过传入 `filteredValue` 直接设置筛选值 */
confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
/** 清除筛选值、临时筛选值 */
clear: (props?: { closeDropdown?: boolean }) => void;
/** 关闭 dropdown */
close: () => void;
/** 把临时筛选值还原为已生效的筛选值 */
reset: () => void;
/** 筛选器配置项,如不需要可以不传 */
filters?: RenderDropdownProps['filters'];
}
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | |
| 10 KB | 姜姜鹏志 | 2019-08-02 | |
| 20 KB | 姜姜鹏志 | 2019-08-12 | |
| 30 KB | 姜姜鹏志 | 2019-08-22 | |
| 40 KB | 姜姜鹏志 | 2019-09-01 | |
| 50 KB | 姜姜鹏志 | 2019-09-11 | |
| 60 KB | 姜姜鹏志 | 2019-09-21 | |
| 70 KB | 姜姜鹏志 | 2019-10-01 | |
| 80 KB | 姜姜鹏志 | 2019-10-11 | |
| 90 KB | 姜姜鹏志 | 2019-10-21 |
代码
import { useState, useRef } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Input, Button, Space, type InputHandle, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const inputRef = useRef<InputHandle>(null);
const figmaIconUrl = '/assets/placeholder/app.svg';
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdown: (props) => {
const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
const handleChange = (value: string) => {
const filteredValue = value ? [value] : [];
setTempFilteredValue(filteredValue);
};
return (
<Space vertical align="start" style={{ padding: 8 }}>
<Input ref={inputRef} value={tempFilteredValue[0]} onChange={handleChange} />
<Space>
<Button onClick={() => confirm({ closeDropdown: true })}>筛选 + 关闭</Button>
<Button onClick={() => clear({ closeDropdown: true })}>清除 + 关闭</Button>
<Button onClick={() => close()}>直接关闭</Button>
</Space>
</Space>
);
},
onFilterDropdownVisibleChange: () => {
if (inputRef.current && inputRef.current.focus) {
inputRef.current.focus();
}
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
onFilter: (value, record) => record.owner.includes(value),
defaultFilteredValue: ['姜鹏志'],
renderFilterDropdown: (props) => {
const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
const handleChange = (value: string) => {
if (value) {
setTempFilteredValue([value]);
} else {
setTempFilteredValue([]);
}
};
return (
<Space vertical align="start" style={{ padding: 8 }}>
<Input value={tempFilteredValue[0]} onChange={handleChange} />
<Space>
<Button onClick={() => confirm({ closeDropdown: false })}>筛选后不关闭</Button>
<Button onClick={() => clear({ closeDropdown: false })}>清除后不关闭</Button>
<Button onClick={() => close()}>直接关闭</Button>
</Space>
</Space>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
return <Table columns={columns} dataSource={dataSource} />;
}筛选确认模式
通过设置 filterConfirmMode='confirm',可以让筛选下拉面板支持确认模式。在该模式下:
- 点击筛选项不会立即生效,而是先暂存到临时状态
- 下拉面板底部会显示"确定"和"重置"按钮
- 点击"确定"按钮后才会应用筛选条件并关闭下拉面板
- 点击"重置"按钮会恢复到打开下拉面板时的初始状态(不会关闭面板)
这个功能适用于需要多选筛选条件后再一次性应用的场景,避免每次点击都触发筛选。
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-01-25 | |
| 5 KB | 郝郝宣 | 2024-01-30 | |
| 10 KB | 姜姜鹏志 | 2024-02-04 | |
| 15 KB | 郝郝宣 | 2024-02-09 | |
| 20 KB | 姜姜鹏志 | 2024-02-14 | |
| 25 KB | 郝郝宣 | 2024-02-19 | |
| 30 KB | 姜姜鹏志 | 2024-02-24 | |
| 35 KB | 郝郝宣 | 2024-02-29 | |
| 40 KB | 姜姜鹏志 | 2024-03-05 | |
| 45 KB | 郝郝宣 | 2024-03-10 |
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 20; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 首页${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const figmaIconUrl = '/assets/placeholder/app.svg';
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design',
},
{
text: 'Semi Pro 设计稿',
value: 'Semi Pro',
},
],
onFilter: (value, record) => record.name.includes(value),
filterMultiple: true,
filterConfirmMode: 'confirm',
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
filters: [
{
text: '姜鹏志',
value: '姜鹏志',
},
{
text: '郝宣',
value: '郝宣',
},
],
onFilter: (value, record) => record.owner.includes(value),
filterMultiple: true,
filterConfirmMode: 'confirm',
render: (text, record) => {
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');
},
},
];
return <Table columns={columns} dataSource={dataSource} />;
}自定义筛选项渲染
支持往 column 中传入 renderFilterDropdownItem 自定义每个筛选项的渲染方式。
text: ReactNode当前筛选项的文案;value: any当前筛选项的值;checked: boolean当前筛选项是否已经选中;filteredValue: any[]当前所有的筛选值;level: number当前筛选项所处层级,如果是嵌套的筛选项,该值会 >= 1;filterMultiple: boolean当前筛选项是否为多选。
| 所有者 |
|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | ||
| 5 KB | 郝郝宣 | 2019-07-28 | ||
| 10 KB | 姜姜鹏志 | 2019-08-02 | ||
| 15 KB | 郝郝宣 | 2019-08-07 | ||
| 20 KB | 姜姜鹏志 | 2019-08-12 | ||
| 25 KB | 郝郝宣 | 2019-08-17 | ||
| 30 KB | 姜姜鹏志 | 2019-08-22 | ||
| 35 KB | 郝郝宣 | 2019-08-27 | ||
| 40 KB | 姜姜鹏志 | 2019-09-01 | ||
| 45 KB | 郝郝宣 | 2019-09-06 |
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Dropdown, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdownItem: ({ text, checked, onChange }) => (
<Dropdown.Item onClick={onChange} active={checked}>
{text}
</Dropdown.Item>
),
filterDropdownProps: {
showTick: true,
},
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: 'owner',
render: (text, record) => {
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: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(getData);
const rowSelection = useMemo<RowSelectionProps<Row>>(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
}),
[]
);
const scroll = useMemo(() => ({ y: 300 }), []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}可以展开的表格
一般可展开行
如果需要渲染可以展开的表格,除了需要在 Table 传 expandedRowRender 这个方法外,还必须要指定 rowKey(默认为 key),Table 会根据 rowKey 取得行唯一标识符。
- 如果
rowKey为Function,则会把rowKey(record)的结果作为行唯一 ID - 如果
rowKey为string类型,则会把record[rowKey]作为行唯一 ID
| 标题 | 大小 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | |||
| 2M | 郝郝宣 | 2020-01-17 05:31 | |||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
width: 500,
dataIndex: 'name',
render: (text, record) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const expandData: Record<number, { key: string; value: ReactNode }[]> = {
0: [
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>设计</Tag> },
{ key: '认证状态', value: '未认证' },
],
1: [
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>模板</Tag> },
{ key: '认证状态', value: '已认证' },
],
2: [
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>文档</Tag> },
{ key: '认证状态', value: '已认证' },
],
};
export default function TableDemo() {
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => {
return <Descriptions align="justify" data={expandData[index]} />;
};
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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);
},
};
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowSelection={rowSelection} pagination={false} />;
}展开按钮渲染为单独列
默认情况,展开按钮会与第一列文案渲染在同一个单元格内,你可以将 hideExpandedColumn 设为 false 来渲染为单独一列:
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
width: 500,
dataIndex: 'name',
render: (text, record) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const expandData: Record<number, { key: string; value: ReactNode }[]> = {
0: [
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>设计</Tag> },
{ key: '认证状态', value: '未认证' },
],
1: [
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>模板</Tag> },
{ key: '认证状态', value: '已认证' },
],
2: [
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>文档</Tag> },
{ key: '认证状态', value: '已认证' },
],
};
export default function TableDemo() {
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => {
return <Descriptions align="justify" data={expandData[index]} />;
};
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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);
},
};
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} hideExpandedColumn={false} rowSelection={rowSelection} pagination={false} />;
}关闭某一行的可展开按钮渲染
可传入 rowExpandable 方法,入参为 record,判断返回值是否为 false 来关闭某一行的可展开按钮的渲染。
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
width: 500,
dataIndex: 'name',
render: (text, record) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const expandData: Record<number, { key: string; value: ReactNode }[]> = {
0: [
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>设计</Tag> },
{ key: '认证状态', value: '未认证' },
],
1: [
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>模板</Tag> },
{ key: '认证状态', value: '已认证' },
],
2: [
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: <Tag style={{ margin: 0 }}>文档</Tag> },
{ key: '认证状态', value: '已认证' },
],
};
export default function TableDemo() {
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => {
return <Descriptions align="justify" data={expandData[index]} />;
};
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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);
},
};
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowExpandable={(record) => record.name !== '设计文档'} hideExpandedColumn={false} rowSelection={rowSelection} pagination={false} />;
}树形数据展示
表格支持树形数据的展示,当数据中有 children 字段时会自动展示为树形表格,如果不需要或使用其他字段可以用 childrenRecordName 进行配置。另外可以通过设置 indentSize 以控制每一层的缩进宽度。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
树形数据简单示例
| Key | 名称 | 数据类型 | 描述 | 默认值 |
|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
代码
import { Table } from '@fibrous/semi-ui-react';
export default function TableDemo() {
const columns = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
return <Table columns={columns} defaultExpandAllRows dataSource={data} />;
}行可交换的树形数据
你可以通过改变 dataSource 元素的顺序来实现行交换操作。
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| status | 视频状态 | Enum <Integer> 枚举 | 视频的可见、推荐状态 | 1 | |
| vid | 视频 ID | String 字符串 | 标识视频的唯一 ID | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 | |
| title | 视频标题 | String 字符串 | 视频的标题 | 无 | |
| video_description | 视频描述 | String 字符串 | 视频的描述 | 无 |
代码
import { useState, type ComponentProps } from 'react';
import { IconArrowUp, IconArrowDown } from '@fibrous/semi-icons-react';
import { Table, Button, type ColumnProps } from '@fibrous/semi-ui-react';
type TreeRow = {
key?: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
};
const raw: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
const rowKey = 'key';
const childrenRecordName = 'children';
export default function TableDemo() {
const [expandedRowKeys, setExpandedRowKeys] = useState([1, 2]);
const [data, setData] = useState(raw);
const switchRecord = (key1?: number, key2?: number) => {
const newData = [...data];
if (key1 != null && key2 != null) {
const item1 = findRecordByKey(key1, newData);
const item2 = findRecordByKey(key2, newData);
const copiedItem1 = { ...item1 };
const copiedItem2 = { ...item2 };
coverRecord(item1, copiedItem2);
coverRecord(item2, copiedItem1);
setData(newData);
}
};
const findRecordByKey = (key: number, data: TreeRow[]): TreeRow | undefined => {
if (Array.isArray(data) && data.length && key != null) {
for (let item of data) {
if (item[rowKey] === key) {
return item;
}
const children = item[childrenRecordName];
if (Array.isArray(children) && children.length) {
const item = findRecordByKey(key, children);
if (item != null) {
return item;
}
}
}
}
};
const coverRecord = (obj?: Record<string, unknown>, srcObj?: Record<string, unknown>) => {
if (obj && typeof obj === 'object' && srcObj && typeof srcObj === 'object') {
const srcKeys = Object.keys(srcObj);
const copied = { ...srcObj };
Object.assign(obj, copied);
Object.keys(obj).forEach((key) => {
if (!srcKeys.includes(key)) {
delete obj[key];
}
});
}
return obj;
};
const getSameLevelRecords = (key: number | undefined, data: TreeRow[] = []): TreeRow[] => {
if (key != null && Array.isArray(data) && data.length) {
if (data.find((item) => item[rowKey] === key)) {
return data;
}
for (let item of data) {
const records = getSameLevelRecords(key, item[childrenRecordName]);
if (records.length) {
return records;
}
}
}
return [];
};
const columns: ColumnProps<TreeRow>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
{
key: 'operation',
render: (record: TreeRow) => {
const records = getSameLevelRecords(record[rowKey], data);
const index = records.findIndex((item) => item[rowKey] === record[rowKey]);
const upProps: ComponentProps<typeof Button> = {};
const downProps: ComponentProps<typeof Button> = {};
if (index > 0) {
const upRow = records[index - 1];
upProps.onClick = () => switchRecord(record[rowKey], upRow[rowKey]);
} else {
upProps.disabled = true;
}
if (index < records.length - 1) {
const downRow = records[index + 1];
downProps.onClick = () => switchRecord(record[rowKey], downRow[rowKey]);
} else {
downProps.disabled = true;
}
return (
<>
<Button aria-label="上移" icon={<IconArrowUp />} {...upProps} />
<Button aria-label="下移" icon={<IconArrowDown />} {...downProps} />
</>
);
},
},
];
return <Table columns={columns} dataSource={data} rowKey={rowKey} childrenRecordName={childrenRecordName} expandedRowKeys={expandedRowKeys} onExpandedRowsChange={(rows = []) => setExpandedRowKeys(rows.flatMap((item) => ('key' in item && item.key !== undefined ? [item.key] : [])))} />;
}树形选择
默认情况下,表格的行选中是各自独立的,你可以通过定义 selectedRowKeys 来模拟一个树形选中。
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
代码
import { useMemo, useState, useCallback } from 'react';
import { union, pull } from 'es-toolkit';
import { Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
type TreeRow = {
key: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
};
type RowKey = string | number;
const childrenRecordName = 'children';
const rowKey = 'key';
const getKey = (record: TreeRow): RowKey => record[rowKey];
const traverse = (data: TreeRow[], res: RowKey[]) => {
for (let record of data) {
const children = record?.children;
res.push(getKey(record));
if (Array.isArray(children)) {
traverse(children, res);
}
}
};
const getAllRowKeys = (data: TreeRow[]) => {
const allRowKeys: RowKey[] = [];
traverse(data, allRowKeys);
return allRowKeys;
};
const findShouldSelectRowKeys = (record: TreeRow, selected: boolean, selectedRowKeys: RowKey[]) => {
let shouldSelectRowKeys: RowKey[];
const children = record.children;
const childrenRowKeys: RowKey[] = [];
if (Array.isArray(children)) {
traverse(children, childrenRowKeys);
}
const key = getKey(record);
if (!selected) {
shouldSelectRowKeys = [...selectedRowKeys];
pull(shouldSelectRowKeys, [key, ...childrenRowKeys]);
} else {
shouldSelectRowKeys = union(selectedRowKeys, [key, ...childrenRowKeys]);
}
return shouldSelectRowKeys;
};
export default function TableDemo() {
const [selectedRowKeys, setSelectedRowKeys] = useState<RowKey[]>([]);
const columns = useMemo<ColumnProps<TreeRow>[]>(
() => [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
],
[]
);
const data = useMemo<TreeRow[]>(
() => [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
key: 121,
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
],
[]
);
const doSelect = useCallback(
(record: TreeRow, selected: boolean) => {
const rowKeys = findShouldSelectRowKeys(record, selected, selectedRowKeys);
setSelectedRowKeys(rowKeys);
console.log('select', record, rowKeys);
},
[selectedRowKeys]
);
const doSelectAll = useCallback(
(selected: boolean) => {
let rowKeys: RowKey[] = [];
if (selected) {
rowKeys = getAllRowKeys(data);
}
setSelectedRowKeys(rowKeys);
},
[data]
);
const rowSelection = useMemo<RowSelectionProps<TreeRow>>(
() => ({
selectedRowKeys,
onSelect: doSelect,
onSelectAll: doSelectAll,
}),
[selectedRowKeys, doSelect, doSelectAll]
);
return <Table columns={columns} rowKey={rowKey} childrenRecordName={childrenRecordName} rowSelection={rowSelection} dataSource={data} pagination={false} />;
}树形选择关联(checkRelation)
通过设置 rowSelection.checkRelation 为 'related',可以实现父子节点选择关联。选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选)。
| Key | 名称 | 数据类型 | 描述 | |
|---|---|---|---|---|
| videos_info | 视频信息 | Object | 视频的元信息 | |
| text_info | 文本信息 | Object | 文本的元信息 |
代码
import { useMemo, useState } from 'react';
import { Table, type RowSelectionProps } from '@fibrous/semi-ui-react';
export default function TableDemo() {
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
const columns = useMemo(
() => [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 200,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
],
[]
);
const data = useMemo(
() => [
{
key: '1',
dataKey: 'videos_info',
name: '视频信息',
type: 'Object',
description: '视频的元信息',
children: [
{
key: '1-1',
dataKey: 'status',
name: '视频状态',
type: 'Enum',
description: '视频的可见状态',
},
{
key: '1-2',
dataKey: 'vid',
name: '视频 ID',
type: 'String',
description: '标识视频的唯一 ID',
children: [
{
key: '1-2-1',
dataKey: 'video_url',
name: '视频地址',
type: 'String',
description: '视频的唯一链接',
},
],
},
],
},
{
key: '2',
dataKey: 'text_info',
name: '文本信息',
type: 'Object',
description: '文本的元信息',
children: [
{
key: '2-1',
dataKey: 'title',
name: '标题',
type: 'String',
description: '文本标题',
},
{
key: '2-2',
dataKey: 'description',
name: '描述',
type: 'String',
description: '文本描述',
},
],
},
],
[]
);
const rowSelection = useMemo(
(): RowSelectionProps<Record<string, unknown>> => ({
selectedRowKeys,
onChange: (selectedRowKeys = []) => {
setSelectedRowKeys(selectedRowKeys);
},
checkRelation: 'related',
}),
[selectedRowKeys]
);
return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={false} />;
}自定义行或单元格事件以及属性
- 传入
onRow/onHeaderRow可以定义表格或表头行的原生事件或属性。 - 传入
column.onCell/column.onHeaderCell可以定义表格或表头单元格原生事件或属性。
原则上 tr/td/th 上支持的属性或事件都能够被定义。例如下面这个例子:
- 表头的 tr 定义了
onMouseEnter/onMouseLeave - 表格的 tr 定义了
className - 表格的第三行定义了
onClick
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | |
| 5 KB | 郝郝宣 | 2019-07-28 | |
| 10 KB | 姜姜鹏志 | 2019-08-02 | |
| 15 KB | 郝郝宣 | 2019-08-07 | |
| 20 KB | 姜姜鹏志 | 2019-08-12 | |
| 25 KB | 郝郝宣 | 2019-08-17 | |
| 30 KB | 姜姜鹏志 | 2019-08-22 | |
| 35 KB | 郝郝宣 | 2019-08-27 | |
| 40 KB | 姜姜鹏志 | 2019-09-01 | |
| 45 KB | 郝郝宣 | 2019-09-06 |
代码
import { useMemo, useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnHeaderRow, type OnRow } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
export default function TableDemo() {
const columns = useMemo<ColumnProps<Row>[]>(
() => [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: 'owner',
render: (text, record) => {
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 [data] = useState(() => {
const _data: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return _data;
});
const onRow = useMemo<OnRow<Row>>(
() => (record, index) => {
const props = {
className: 'my-tr-class',
};
if (index === 2) {
return {
...props,
onClick: () => console.log('mouse click: ', record, index),
};
} else {
return {
...props,
};
}
},
[]
);
const onHeaderRow = useMemo<OnHeaderRow<Row>>(
() => (columns, index) => {
return {
onMouseEnter: () => console.log('mouse enter: ', columns, index),
onMouseLeave: () => console.log('mouse leave: ', columns, index),
};
},
[]
);
return <Table columns={columns} dataSource={data} onRow={onRow} onHeaderRow={onHeaderRow} />;
}实现斑马纹样式
使用 onRow 给每行设置一个背景色,实现有斑马纹效果的表格。如果设置了固定列,可以通过 onCell 给每列设置一个背景色实现相同效果。
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 | ||
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps, type OnRow } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
export default function TableDemo() {
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi D2C 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: 'Semi D2C 设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const handleRow: OnRow<Row> = (_record, index) => {
if (index % 2 === 0) {
return {
style: {
background: 'var(--semi-color-fill-0)',
},
};
} else {
return {};
}
};
return <Table columns={columns} dataSource={data} onRow={handleRow} pagination={false} />;
}实现表头样式定制
可以通过 Column.onHeaderCell 返回特定 style 或 className,定制表头的样式
如下例子,通过传入 backgroundColor 改变了表头背景色
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
export default function TableDemo() {
const onHeaderCell = () => ({
style: {
backgroundColor: 'var(--semi-color-fill-0)',
},
});
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 280,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
},
{
title: '大小',
width: 100,
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
},
},
];
columns.forEach((item) => (item.onHeaderCell = onHeaderCell));
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
return (
<>
<Table columns={columns} dataSource={data} pagination={false} />
</>
);
}实现单元格 Hover 样式定制
Table 默认为整行配置 Hover 样式,如果你需要修改相关样式可以通过 CSS 覆盖的方式自行实现。
如下例子,通过 CSS 覆盖,将可 Hover 的背景色或者由行高亮改为 Cell 单元格高亮
.component-table-demo-cell-hover-custom {
.semi-table-tbody {
// 去掉行 hover 的原有样式
.semi-table-row:hover {
.semi-table-row-cell {
background-color: transparent;
background-image: none;
&.semi-table-cell-fixed-left::before {
background-color: transparent;
}
}
// 给单个单元格加样式
.semi-table-row-cell:hover {
background-color: var(--semi-light-green-1);
}
}
}
}
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
export default function TableDemo() {
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
},
},
{
title: '大小',
width: 100,
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
},
},
];
const data = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
return (
<>
<Table columns={columns} dataSource={data} className="component-table-demo-cell-hover-custom" pagination={false} />
</>
);
}单元格缩略
使用 ellipsis 可以让单元格自动实现缩略效果。
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 2M | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 2M | 2020-02-02 05:13 | |
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 34KB | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 34KB | 2020-02-02 05:13 |
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, type ColumnProps } from '@fibrous/semi-ui-react';
export default function TableDemo() {
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
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),
ellipsis: true,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
ellipsis: true,
},
{
title: '大小',
dataIndex: 'size',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
ellipsis: true,
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => {
return <IconMore />;
},
},
];
const data = [
{
key: '1',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'red',
},
{
key: '3',
nameIconSrc: '/assets/docs-icon.png',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
size: '34KB',
owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
{
key: '4',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
];
return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}设置 ellipsis.showTitle 为 false 可以隐藏默认原生的 HTML title。配合 column.render 可以自定义内容提示。
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 2M | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 2M | 2020-02-02 05:13 | |
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang | 34KB | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣 | 34KB | 2020-02-02 05:13 |
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Typography, type ColumnProps } from '@fibrous/semi-ui-react';
export default function TableDemo() {
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
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),
ellipsis: { showTitle: false },
render: (text) => <Typography.Text ellipsis={{ showTooltip: true }}>{text}</Typography.Text>,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
ellipsis: { showTitle: false },
render: (text) => <Typography.Text ellipsis={{ showTooltip: true }}>{text}</Typography.Text>,
},
{
title: '大小',
dataIndex: 'size',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
ellipsis: true,
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => {
return <IconMore />;
},
},
];
const data = [
{
key: '1',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'red',
},
{
key: '3',
nameIconSrc: '/assets/docs-icon.png',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
size: '34KB',
owner: 'Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
{
key: '4',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: '郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
];
return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}可伸缩列
基本伸缩列
对于一些内容比较多的列,可以选择打开伸缩列功能,在表头进行拉拽实现列宽的实时变化。
不过你需要注意一些参数:
resizable设定为true或者一个objectcolumns里需要伸缩功能的列都要指定width这个字段(如果不传,该列不具备伸缩功能,且其列宽度会被浏览器自动调整)column.resize可以在 resizable 开启后生效,设置为 false 后,列不再支持伸缩。- 拖动过的列保留拖动后的宽度,没拖动过的列跟随
columns里的width
与固定列同时使用时,需指定某一列不设置宽度
不推荐与
scroll.x同时使用,scroll.x 指定表格是有宽度范围的,而伸缩列会拓展列宽,这可能会导致表格对不齐
标题 | 所有者 | 操作列 | ||
|---|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | ||
| 5 KB | 郝郝宣 | 2019-07-28 | ||
| 10 KB | 姜姜鹏志 | 2019-08-02 | ||
| 15 KB | 郝郝宣 | 2019-08-07 | ||
| 20 KB | 姜姜鹏志 | 2019-08-12 | ||
| 25 KB | 郝郝宣 | 2019-08-17 | ||
| 30 KB | 姜姜鹏志 | 2019-08-22 | ||
| 35 KB | 郝郝宣 | 2019-08-27 | ||
| 40 KB | 姜姜鹏志 | 2019-09-01 | ||
| 45 KB | 郝郝宣 | 2019-09-06 |
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
export default function TableDemo() {
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
resize: false,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: '所有者',
width: 200,
dataIndex: 'owner',
render: (text, record) => {
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');
},
},
{
title: '操作列',
dataIndex: 'operate',
fixed: 'right',
width: 100,
resize: false,
render: () => {
return <IconMore />;
},
},
];
const [data] = useState(() => {
const _data: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return _data;
});
return <Table columns={columns} dataSource={data} resizable bordered />;
}进阶的伸缩列
resizable 还能为一个 Object,包括三个事件方法:
onResizeonResizeStartonResizeStop
分别触发于列宽改变中、开始改变和结束改变三个时机。开发者可以选择在这个时机修改 column,例如在拉拽时增加一个拖动时的竖线效果等,如下例。
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | |
| 5 KB | 郝郝宣 | 2019-07-28 | |
| 10 KB | 姜姜鹏志 | 2019-08-02 | |
| 15 KB | 郝郝宣 | 2019-08-07 | |
| 20 KB | 姜姜鹏志 | 2019-08-12 |
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { addClass, removeClass } from '@fibrous/semi-foundation/utils/classnames';
import { Table, Avatar, type ColumnProps, type TableResizableProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pagination = { pageSize: 5 };
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
export default function TableDemo() {
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: '所有者',
width: 200,
dataIndex: 'owner',
render: (text, record) => {
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 [data] = useState(() => {
const _data: Row[] = [];
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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return _data;
});
const resizable: TableResizableProps<Row> = {
onResizeStart: (curColumn) => {
const className = addClass(curColumn.className, 'my-resizing');
return { className };
},
onResizeStop: (curColumn) => {
const className = removeClass(curColumn.className, 'my-resizing');
return { className };
},
};
return (
<div id="components-table-demo-resizable-column">
<Table columns={columns} dataSource={data} resizable={resizable} pagination={pagination} bordered />
</div>
);
}本例中使用的 CSS 样式定义:
#components-table-demo-resizable-column .my-resizing {
border-right: 2px solid red;
}
#components-table-demo-resizable-column .semi-table-resize-handle:hover {
background-color: red;
}
#components-table-demo-resizable-column .my-resizing:hover .semi-table-resize-handle {
background-color: inherit;
}
拖拽排序
使用 dnd-kit 搭配 components API 可轻松实现拖拽排序。
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | |
| 5 KB | 郝郝宣 | 2019-07-23 | |
| 10 KB | 姜姜鹏志 | 2019-07-23 | |
| 15 KB | 郝郝宣 | 2019-07-23 | |
| 20 KB | 姜姜鹏志 | 2019-07-23 | |
| 25 KB | 郝郝宣 | 2019-07-23 | |
| 30 KB | 姜姜鹏志 | 2019-07-23 | |
| 35 KB | 郝郝宣 | 2019-07-23 | |
| 40 KB | 姜姜鹏志 | 2019-07-23 | |
| 45 KB | 郝郝宣 | 2019-07-23 |
代码
import { useMemo, useState, type ComponentProps, type CSSProperties } from 'react';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { arrayMove } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/react';
import { isSortable, useSortable } from '@dnd-kit/react/sortable';
import clsx from 'clsx';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnRowReturnObject, type TableProps } from '@fibrous/semi-ui-react';
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(2019, 6, 23, 12).valueOf() + randomNumber,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
interface SortableRowProps extends ComponentProps<'tr'> {
index: number;
'data-row-key': string;
}
const SortableRow = (props: SortableRowProps) => {
// index comes from the table's onRow; it must not reach the tr
const { index, ...rowProps } = props;
const { ref, isDragging } = useSortable({ id: props['data-row-key'], index });
const style: CSSProperties = {
...props.style,
cursor: 'grabbing',
...(isDragging ? { zIndex: 999, position: 'relative' } : {}),
};
const rowCls = clsx(props.className, { ['isDragging']: isDragging });
return <tr {...rowProps} className={rowCls} ref={ref} style={style} />;
};
export default function TableDemo() {
const pageSize = 10;
const [dataSource, setData] = useState(getData);
const [pageNum, setPageNum] = useState(1);
const pageData = useMemo(() => dataSource.slice((pageNum - 1) * pageSize, pageNum * pageSize), [dataSource, pageNum]);
const columns = useMemo(
(): ColumnProps[] => [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src="/assets/placeholder/app.svg" alt="" style={{ marginRight: 12 }} />
{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: 'owner',
render: (text, record) => {
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');
},
},
],
[]
);
// Sortable indexes are relative to the current page
const handleDragEnd: ComponentProps<typeof DragDropProvider>['onDragEnd'] = (event) => {
const { source } = event.operation;
if (event.canceled || !isSortable(source) || source.initialIndex === source.index) {
return;
}
const offset = (pageNum - 1) * pageSize;
setData((prev) => arrayMove(prev, offset + source.initialIndex, offset + source.index));
};
const handleChange: TableProps['onChange'] = ({ pagination }) => {
if (pagination?.currentPage) {
setPageNum(pagination.currentPage);
}
};
return (
<DragDropProvider
// Accessibility would add role="button" and a Tab stop to each row; keep table row semantics
plugins={(defaults) => defaults.filter((plugin) => plugin !== Accessibility)}
modifiers={[RestrictToVerticalAxis]}
onDragEnd={handleDragEnd}
>
<Table
components={{
body: {
row: SortableRow,
},
}}
rowKey="key"
columns={columns}
dataSource={pageData}
onRow={(_record, index) => {
const rowProps: OnRowReturnObject & { index: number } = { index };
return rowProps;
}}
pagination={{ currentPage: pageNum, pageSize: pageSize, total: dataSource.length }}
onChange={handleChange}
/>
</DragDropProvider>
);
}表格分组
对于一些数据需要分组展示的表格,可以传入 groupBy 定义分组规则,使用 renderGroupSection 来定义分组表头的渲染。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
标题 | 所有者 |
|---|
| 100 KB | 姜姜鹏志 | 2019-10-31 | |
| 100 KB | 郝郝宣 | 2019-10-31 | |
| 100 KB | 姜姜鹏志 | 2019-10-31 | |
| 112 KB | 郝郝宣 | 2019-11-12 | |
| 112 KB | 姜姜鹏志 | 2019-11-12 | |
| 112 KB | 郝郝宣 | 2019-11-12 | |
| 105 KB | 姜姜鹏志 | 2019-11-05 | |
| 105 KB | 郝郝宣 | 2019-11-05 | |
| 105 KB | 姜姜鹏志 | 2019-11-05 | |
| 117 KB | 郝郝宣 | 2019-11-17 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{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: 'owner',
render: (text, record) => {
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: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = ((i * 1000) % 19) + 100;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
export default function TableDemo() {
const rowKey = (record: Row) => `${record.owner.toLowerCase()}-${record.name.toLowerCase()}`;
return (
<div style={{ padding: '20px 0px' }}>
<Table
dataSource={data}
rowKey={rowKey}
groupBy="size"
columns={columns}
renderGroupSection={(groupKey) => <strong>根据文件大小分组 {groupKey} KB</strong>}
onGroupedRow={(group, index) => {
return {
onClick: () => {
console.log(`Grouped row clicked: `, group, index);
},
};
}}
clickGroupedRowToExpand // 点击整行即可展开
scroll={{ y: 480 }}
/>
</div>
);
}虚拟化表格
虚拟化可用于需要渲染大规模数据的场景,通过配置 virtualized 参数来开启这个功能。需要注意的是:
- 必须传递
scroll.y(number)与style.width(number);
- 需要传递每行的高度
virtualized.itemSize(不传时按size取默认行高:default为53,middle为45,small为37,组头行高与普通行相同),可以为如下类型:number(index, { sectionRow?: boolean, expandedRow?: boolean }) => number
- 表格分组虚拟化已支持
virtualized对象另外支持overscanCount:可视区上下额外渲染的行数,默认 2
通过 getVirtualizedListRef 获取虚拟列表的 ref,ref.current 提供 scrollTo(offset) 与 scrollToItem(index, align),align 为 auto(默认)、smart、center、start、end。
以下为渲染 1000 条数据的示例。
标题 | 所有者 |
|---|
代码
import { useRef } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Button, type ColumnProps, type TableGetVirtualizedListRef } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 200,
fixed: true,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 150,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
fixed: 'right',
width: 150,
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: Row[] = [];
for (let i = 0; i < 1000; 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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
export default function TableDemo() {
const virtualizedListRef = useRef<Parameters<TableGetVirtualizedListRef>[0] | null>(null);
const scroll = { y: 400, x: 900 };
const style = { width: 750, margin: '0 auto' };
return (
<>
<Button onClick={() => virtualizedListRef.current?.current.scrollToItem(100)}>Scroll to 100</Button>
<Table
pagination={false}
columns={columns}
dataSource={data}
scroll={scroll}
style={style}
virtualized
getVirtualizedListRef={(ref) => {
virtualizedListRef.current = ref;
}}
/>
</>
);
}无限滚动
基于虚拟化特性,通过传入 virtualized.onScroll 我们可以实现无限滚动加载数据。
标题 | 所有者 |
|---|
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type TableVirtualizedProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 200,
fixed: true,
render: (text) => {
return <div>{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: 150,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
fixed: 'right',
width: 150,
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const pageSize = 20; // 每次加载 20 条
const getPage = (start: number) => {
const page: Row[] = [];
for (let i = start; i < start + pageSize; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
page.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return page;
};
export default function TableDemo() {
const [data, setData] = useState(() => getPage(0));
const scroll = { y: 600, x: 1000 };
const style = { width: 750, margin: '0 auto' };
const loadMore = () => {
setData([...data, ...getPage(data.length)]);
};
const itemSize = 56;
const virtualized: TableVirtualizedProps = {
itemSize,
onScroll: ({ scrollDirection, scrollOffset = 0, scrollUpdateWasRequested }) => {
if (scrollDirection === 'forward' && scrollOffset >= (data.length - Math.ceil(scroll.y / itemSize) * 1.5) * itemSize && !scrollUpdateWasRequested) {
loadMore();
}
},
};
return <Table pagination={false} columns={columns} dataSource={data} scroll={scroll} style={style} virtualized={virtualized} />;
}受控的动态表格
标题 | 所有者 |
|---|
代码
import { useState, useEffect, useCallback, type ComponentProps, type CSSProperties, type ReactNode } from 'react';
import * as dateFns from 'date-fns';
import { Table, Switch, ButtonGroup, Button, Avatar, type TableChangeInfo, type ColumnProps, type RowSelectionProps, type TableScroll, type TablePaginationProps, type TableProps, type TablePaginationPosition as PaginationPosition } from '@fibrous/semi-ui-react';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: string;
description?: string;
}
interface PageState extends TablePaginationProps {
currentPage: number;
pageSize: number;
total: number;
}
interface FetchResult {
dataSource: Row[];
pagination: PageState;
sorter: ColumnProps;
filters: ColumnProps[];
}
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const dataTotalSize = 46;
const wrapStyle: CSSProperties = {
marginBottom: 15,
display: 'flex',
justifyContent: 'space-around',
flexWrap: 'wrap',
};
const initialColumns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</span>
);
},
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: 'owner',
render: (text, record) => {
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 allData = Array.from({ length: dataTotalSize }, (_v, i): Row => {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
return {
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
description: `这是第 ${i} 份设计稿的详细说明。`,
};
});
const defaultPagination: PageState = {
currentPage: 1,
pageSize: 8,
total: allData.length,
};
const renderExpandedRow = (record: Row) => {
return {
children: <p>{record.description}</p>,
fixed: 'left' as const,
};
};
const renderFooter = () => <p style={{ margin: 0 }}>This is footer.</p>;
const mergeColumns = (column: ColumnProps, columns: ColumnProps[], keys: (keyof ColumnProps)[] = ['dataIndex']) => {
const nextColumns = [...columns];
nextColumns.forEach((curColumn, index) => {
let isTarget = !!(keys && keys.length);
for (const key of keys) {
if (column[key] !== curColumn[key]) {
isTarget = false;
break;
}
}
if (isTarget) {
nextColumns[index] = { ...curColumn, ...column };
}
});
return nextColumns;
};
const filterData = (filters: ColumnProps[], dataSource: Row[]) => {
let result = [...dataSource];
filters.forEach((filter) => {
const filteredValue = filter.filteredValue;
const dataIndex = filter.dataIndex;
if (Array.isArray(filteredValue) && filteredValue.length && dataIndex) {
result = result.filter((data) => filteredValue.filter((value) => String(data[dataIndex as keyof Row]).indexOf(value) > -1).length);
}
});
return result;
};
const sortData = (sortObj: ColumnProps, dataSource: Row[]) => {
const { sortOrder, dataIndex = '' } = sortObj;
let sorter = sortObj.sorter;
let result = dataSource;
if (sorter && sortOrder && typeof sorter !== 'function') {
sorter = (a, b) => (a[dataIndex] > b[dataIndex] ? 1 : -1);
}
if (typeof sorter === 'function') {
result = [...result].sort(sorter);
if (sortOrder === 'descend') {
result = result.reverse();
}
}
return result;
};
const fetchData = (pagination: PageState, currentPage = 1, sorter: ColumnProps = {}, filters: ColumnProps[] = []) => {
const nextPagination = { ...pagination, currentPage };
return new Promise<FetchResult>((resolve) => {
setTimeout(() => {
let data = sortData(sorter, allData);
data = filterData(filters, data);
const dataSource = data.slice((currentPage - 1) * nextPagination.pageSize, currentPage * nextPagination.pageSize);
nextPagination.total = data.length;
resolve({
dataSource,
pagination: nextPagination,
sorter,
filters,
});
}, 1500);
});
};
const getSorterColumn = (columns: ColumnProps[]) => columns.filter((column) => !!column.sorter)[0];
const getFilterColumns = (columns: ColumnProps[]) => columns.filter((column) => Array.isArray(column.filteredValue) && column.filteredValue.length);
interface TableSwitchProps {
text: string;
children?: ReactNode;
checked?: boolean | number | string;
onChange?: (checked: boolean) => void;
style?: CSSProperties;
}
function TableSwitch({ text, children, checked, onChange, style = { display: 'inline-flex', alignItems: 'center', margin: 5 } }: TableSwitchProps) {
const switchProps: ComponentProps<typeof Switch> = { onChange };
if (checked != null) {
switchProps.checked = !!checked;
}
return (
<span style={style}>
<span>{text}</span>
{children != null ? children : <Switch size="small" aria-label={text} {...switchProps} />}
</span>
);
}
export default function TableDemo() {
const [loading, setLoading] = useState(true);
const [columns, setColumns] = useState(initialColumns);
const [scroll, setScroll] = useState<TableScroll>({});
const [rowSelection, setRowSelection] = useState<RowSelectionProps<Row>>();
const [expandable, setExpandable] = useState(false);
const [expandCellFixed, setExpandCellFixed] = useState(false);
const [showTitle, setShowTitle] = useState(false);
const [showFooter, setShowFooter] = useState(false);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [showHeader, setShowHeader] = useState(true);
const [bordered, setBordered] = useState(false);
const [resizable, setResizable] = useState(false);
const [pagination, setPagination] = useState<PageState | false>(defaultPagination);
const [dataSource, setDataSource] = useState<Row[]>([]);
const load = useCallback((currentPagination: PageState, currentPage?: number, sorter?: ColumnProps, filters?: ColumnProps[]) => {
fetchData(currentPagination, currentPage, sorter, filters)
.then((result) => {
setColumns((prevColumns) => {
let nextColumns = mergeColumns(result.sorter, prevColumns);
for (const filterObj of result.filters) {
nextColumns = mergeColumns(filterObj, nextColumns);
}
return nextColumns;
});
setLoading(false);
setPagination(result.pagination);
setDataSource(result.dataSource);
})
.catch((err) => {
console.error(err);
setLoading(false);
});
}, []);
const setPage = (page?: number | null, sorter?: ColumnProps | null, filters?: ColumnProps[]) => {
if (loading) {
return;
}
const currentPage = typeof page === 'number' ? page : (pagination && pagination.currentPage) || 1;
setLoading(true);
load(pagination || defaultPagination, currentPage, sorter || getSorterColumn(columns), filters || getFilterColumns(columns));
};
useEffect(() => {
load(defaultPagination, 1, getSorterColumn(initialColumns), []);
}, [load]);
const toggleFixHeader = (checked: boolean) => {
setScroll({ ...scroll, y: checked ? 300 : undefined });
};
const toggleFixColumns = (checked: boolean) => {
const lastIndex = columns.length - 1;
if (checked) {
setColumns(
columns.map((column, index) => {
if (index === 0) {
return { ...column, fixed: true };
}
return index === lastIndex ? { ...column, fixed: 'right' } : column;
})
);
} else {
setColumns(columns.map((column) => ({ ...column, fixed: false })));
}
if (rowSelection) {
setRowSelection({ ...rowSelection, fixed: checked });
}
setScroll({ ...scroll, x: checked ? '150%' : undefined });
setExpandCellFixed(checked);
};
const toggleRowSelection = (checked: boolean) => {
setRowSelection(
checked
? {
width: 48,
fixed: true,
onChange: (selectedRowKeys, selectedRows) => console.log('Selection changed, selectedRowKeys: ', selectedRowKeys, 'selectedRows: ', selectedRows),
}
: undefined
);
};
const toggleShowSorter = (checked: boolean) => {
setColumns(
columns.map((column) => ({
...column,
sorter: checked ? initialColumns.find((item) => item.dataIndex === column.dataIndex)?.sorter : undefined,
}))
);
};
const toggleShowFilter = (checked: boolean) => {
if (checked) {
setColumns(
columns.map((column) =>
column.dataIndex === 'name'
? {
...column,
filters: [
{
text: '姓名中包含 1',
value: '1',
},
{
text: '姓名中包含 2',
value: '2',
},
{
text: '姓名中包含 3',
value: '3',
},
],
filteredValue: [],
}
: column
)
);
} else {
setColumns(columns.map((column) => ({ ...column, filters: undefined, filteredValue: undefined })));
setPage(null, null, []);
}
};
const onChange = (data: TableChangeInfo<Row>) => {
console.log('Table changed: ', data);
const { pagination: tablePagination, sorter, filters } = data;
setPage(tablePagination?.currentPage, sorter, filters);
};
const onExpandedRowsChange: TableProps<Row>['onExpandedRowsChange'] = (rows) => {
console.log('Expanded rows changed to: ', rows);
// The demo has no grouped rows, so every entry is a data row
setExpandedRowKeys((Array.isArray(rows) && rows.map((row) => (row as Row).key)) || []);
};
const toggleExpandedRowKeys = (checked: boolean) => {
if (checked) {
setExpandedRowKeys(dataSource.filter((data) => data.key).map((data) => data.key));
setExpandable(true);
} else {
setExpandedRowKeys([]);
}
};
const toggleResizable = (checked: boolean) => {
setResizable(!!checked);
setBordered(!!checked);
};
const toggleDataSource = (checked: boolean) => {
if (checked) {
setDataSource([]);
} else {
setPage();
}
};
const switchPagination = (position: PaginationPosition | false) => {
setPagination(position ? { ...defaultPagination, ...pagination, position } : false);
};
return (
<div>
<div style={wrapStyle}>
<TableSwitch text="固定表头:" checked={scroll && scroll.y} onChange={toggleFixHeader} />
<TableSwitch text="隐藏表头:" onChange={(checked) => setShowHeader(!checked)} />
<TableSwitch text="显示标题:" onChange={setShowTitle} />
<TableSwitch text="显示底部:" onChange={setShowFooter} />
<TableSwitch text="固定列:" onChange={toggleFixColumns} />
<TableSwitch text="显示选择列:" onChange={toggleRowSelection} />
<TableSwitch text="显示加载状态:" onChange={setLoading} checked={loading} />
<TableSwitch text="无数据:" onChange={toggleDataSource} checked={!dataSource || !dataSource.length} />
<TableSwitch text="开启排序功能:" onChange={toggleShowSorter} checked={columns.some((column) => column.sorter)} />
<TableSwitch text="开启过滤功能:" onChange={toggleShowFilter} checked={columns.some((column) => column.filters)} />
<TableSwitch text="开启行展开功能:" onChange={setExpandable} checked={expandable} />
<TableSwitch text="展开当前所有行:" onChange={toggleExpandedRowKeys} />
<TableSwitch text="显示边框:" onChange={setBordered} checked={bordered} />
<TableSwitch text="开启列伸缩功能:" onChange={toggleResizable} />
<TableSwitch text="分页控件:">
<ButtonGroup>
<Button onClick={() => switchPagination('bottom')}>Bottom</Button>
<Button onClick={() => switchPagination('top')}>Top</Button>
<Button onClick={() => switchPagination('both')}>Both</Button>
<Button onClick={() => switchPagination(false)}>None</Button>
</ButtonGroup>
</TableSwitch>
</div>
<Table
onExpandedRowsChange={onExpandedRowsChange}
title={showTitle ? 'This is title.' : null}
footer={showFooter ? renderFooter : null}
showHeader={showHeader}
bordered={bordered}
onChange={onChange}
expandCellFixed={expandCellFixed}
expandedRowRender={expandable ? renderExpandedRow : undefined}
expandedRowKeys={expandedRowKeys}
rowSelection={rowSelection}
scroll={scroll}
columns={columns}
dataSource={dataSource}
pagination={pagination && { ...pagination, onPageChange: setPage }}
loading={loading}
resizable={resizable}
/>
</div>
);
}完全自定义渲染
一般情况下,使用 Column.render 即可,但是你也可以通过传递 Column.useFullRender=true 来开启完全自定义渲染模式,此时复选框按钮、展开按钮、缩进等组件将会透传至 Column.title 与 Column.render 方法中,你可以进一步来定义表头和单元格的内容渲染方式。
其中 Column.title 接受的入参为:
{
filter: ReactNode, // 筛选按钮
sorter: ReactNode, // 排序按钮
selection: ReactNode, // 选择按钮
}
Column.render 第四个入参为一个 object,结构如下:
{
expandIcon: ReactNode, // 展开按钮
selection: ReactNode, // 选择按钮
indentTex: ReactNode, // 缩进
}
下方的例子则是将复选框与内容渲染至同一单元格和表头中。
| Name | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2019-07-23 | |
| 5 KB | 郝郝宣 | 2019-07-28 | |
| 10 KB | 姜姜鹏志 | 2019-08-02 | |
| 15 KB | 郝郝宣 | 2019-08-07 | |
| 20 KB | 姜姜鹏志 | 2019-08-12 | |
| 25 KB | 郝郝宣 | 2019-08-17 | |
| 30 KB | 姜姜鹏志 | 2019-08-22 | |
| 35 KB | 郝郝宣 | 2019-08-27 | |
| 40 KB | 姜姜鹏志 | 2019-09-01 | |
| 45 KB | 郝郝宣 | 2019-09-06 | |
| 50 KB | 姜姜鹏志 | 2019-09-11 | |
| 55 KB | 郝郝宣 | 2019-09-16 |
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: ({ sorter, filter, selection }) => (
<span style={{ display: 'inline-flex', alignItems: 'center', paddingLeft: 20 }}>
{selection}
<span style={{ marginLeft: 8 }}>Name</span>
{sorter}
{filter}
</span>
),
dataIndex: 'name',
width: 400,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
useFullRender: true,
// The fourth render parameter holds the expand button, selection button and text
render: (text, _record, _index, { expandIcon, selection, indentText }) => {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
{indentText}
{expandIcon}
{selection}
<span style={{ marginLeft: 8 }}>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</span>
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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 = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; 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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(() => getData(46));
const pagination = useMemo(
() => ({
pageSize: 12,
}),
[]
);
const rowSelection = useMemo<RowSelectionProps<Row>>(() => {
return {
hidden: true,
fixed: 'left',
};
}, []);
return <Table pagination={pagination} rowSelection={rowSelection} columns={columns} dataSource={dataSource} onChange={(...args) => console.log(...args)} expandedRowRender={(record) => <article>{record.name}</article>} />;
}表头合并
用户可以通过表头合并功能进行表头的分组,表头合并支持与固定列、虚拟化、数据分组、列伸缩等功能复合使用,也同时支持 JSX 或者配置式写法。
合并表头配置式写法
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| 0 KB | 姜姜鹏志 | 2019-07-23 | |||
| 5 KB | 郝郝宣 | 2019-07-28 | |||
| 10 KB | 姜姜鹏志 | 2019-08-02 | |||
| 15 KB | 郝郝宣 | 2019-08-07 | |||
| 20 KB | 姜姜鹏志 | 2019-08-12 | |||
| 25 KB | 郝郝宣 | 2019-08-17 | |||
| 30 KB | 姜姜鹏志 | 2019-08-22 | |||
| 35 KB | 郝郝宣 | 2019-08-27 | |||
| 40 KB | 姜姜鹏志 | 2019-09-01 | |||
| 45 KB | 郝郝宣 | 2019-09-06 |
代码
import { useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '基本信息',
fixed: 'left',
children: [
{
title: '标题',
dataIndex: 'name',
width: 300,
fixed: true,
render: (text) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</span>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 100,
fixed: true,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
],
},
{
title: '其他信息',
children: [
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
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');
},
},
],
},
{
title: '更多',
fixed: 'right',
width: 100,
align: 'center',
dataIndex: 'operate',
render: () => {
return <IconMore />;
},
},
];
const getData = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; 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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const data = useMemo(() => {
const _data = getData(46);
return _data;
}, []);
return <Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={data} scroll={{ x: 1000, y: 400 }} onChange={(...args) => console.log(...args)} columns={columns} />;
}合并表头 JSX 写法
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| 0 KB | 姜姜鹏志 | 2019-07-23 | |||
| 5 KB | 郝郝宣 | 2019-07-28 | |||
| 10 KB | 姜姜鹏志 | 2019-08-02 | |||
| 15 KB | 郝郝宣 | 2019-08-07 | |||
| 20 KB | 姜姜鹏志 | 2019-08-12 | |||
| 25 KB | 郝郝宣 | 2019-08-17 | |||
| 30 KB | 姜姜鹏志 | 2019-08-22 | |||
| 35 KB | 郝郝宣 | 2019-08-27 | |||
| 40 KB | 姜姜鹏志 | 2019-09-01 | |||
| 45 KB | 郝郝宣 | 2019-09-06 |
代码
import { useMemo, type ReactNode } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar } from '@fibrous/semi-ui-react';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const Column = Table.Column;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; 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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const data = useMemo(() => {
const _data = getData(46);
return _data;
}, []);
const nameFilters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const renderName = (text: ReactNode) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</span>
);
};
const renderOwner = (text: ReactNode, record: Row) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
return (
<Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={data} scroll={{ x: 1000, y: 400 }} onChange={(...args) => console.log(...args)}>
<Column title="基本信息" fixed="left">
<Column<Row> title="标题" dataIndex="name" width={300} fixed render={renderName} filters={nameFilters} onFilter={(value, record) => record.name.includes(value)} />
<Column<Row> title="大小" dataIndex="size" width={100} fixed render={(text) => `${text} KB`} sorter={(a, b) => (a.size - b.size > 0 ? 1 : -1)} />
</Column>
<Column title="其他信息">
<Column title="所有者" dataIndex="owner" render={renderOwner} />
<Column<Row> title="更新日期" dataIndex="updateTime" sorter={(a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1)} render={(value) => dateFns.format(new Date(value), 'yyyy-MM-dd')} />
</Column>
<Column title="更多" dataIndex="operate" fixed="right" width={100} align="center" render={() => <IconMore />} />
</Table>
);
}行列合并
- 表头除了通过
children写法进行合并外,可通过设置column.colSpan进行表头的列合并。 - 表格支持行/列合并,使用
render里的单元格属性colSpan或者rowSpan设值为 0 时,设置的表格不会渲染。
type Render = (text: string, record: Object, index: number, options?: RenderOptions) => {
children: React.ReactNode;
// 合并进 td 的属性,colSpan 或 rowSpan 为 0 时不渲染该单元格
props: React.TdHTMLAttributes<HTMLElement>,
}
interface RenderOptions {
expandIcon?: React.ReactNode;
isHovering?: boolean;
}
| 标题 | 大小 | 所有者 | |
|---|---|---|---|
| 5 KB | 郝郝宣 | 2019-07-28 | |
姜姜鹏志 | |||
| 15 KB | 郝郝宣 | 2019-08-07 | |
| 20 KB | 姜姜鹏志 | 2019-08-12 | |
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RenderReturnObject } from '@fibrous/semi-ui-react';
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, _record, index) => {
const children = (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
{text}
</div>
);
const renderObject: RenderReturnObject = { children, props: {} };
if (index === 0) {
renderObject.props = {
colSpan: 4,
};
}
if (index === 1) {
renderObject.props = {
rowSpan: 2,
};
}
if (index === 2) {
renderObject.props = {
rowSpan: 0,
};
}
return renderObject;
},
},
{
title: '大小',
dataIndex: 'size',
render: (text, _record, index) => {
if (index === 0) {
return {
children: `${text} KB`,
props: {
colSpan: 0,
},
};
}
if (index === 1) {
return {
children: `${text} KB`,
props: {
rowSpan: 2,
},
};
}
if (index === 2) {
return {
children: `${text} KB`,
props: {
rowSpan: 0,
},
};
}
return `${text} KB`;
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record, index) => {
const children = (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
if (index === 0) {
return {
children,
props: {
colSpan: 0,
},
};
}
return children;
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value, _record, index) => {
const children = dateFns.format(new Date(value), 'yyyy-MM-dd');
if (index === 0) {
return {
children,
props: {
colSpan: 0,
},
};
}
if (index === 1) {
return {
children,
props: {
rowSpan: 2,
},
};
}
if (index === 2) {
return {
children,
props: {
rowSpan: 0,
},
};
}
return children;
},
},
];
const DAY = 24 * 60 * 60 * 1000;
const getData = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; 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(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
export default function TableDemo() {
const [dataSource] = useState(() => getData(5));
return <Table columns={columns} dataSource={dataSource} pagination={false} />;
}API 参考
Table
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| bordered | 是否展示外边框和列边框。 | boolean | false |
| children | Table.Column 子项,未传入 columns 时读取它们。 | ReactNode | - |
| childrenRecordName | 树形表格中,数据项里表示子级数据的字段名。 | string | "children" |
| className | 最外层容器的类名。 | string | "" |
| clickGroupedRowToExpand | 点击分组行时展开或收起该组。 | boolean | - |
| columns | 列配置,详见 Column。传入时(包括空数组)优先于 Table.Column 子项。 | TypeColumnProps<RecordType>[] | - |
| components | 覆盖 Table 的组成元素,如 table、header.cell、body.row,值为标签名或组件;组件接收被替换元素的属性。 | TableComponents | - |
| dataSource | 数据。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。 | RecordType[] | [] |
| defaultExpandAllGroupRows | 默认是否展开所有分组行,动态加载数据时不生效。 | boolean | false |
| defaultExpandAllRows | 默认是否展开所有行,动态加载数据时不生效。 | boolean | false |
| defaultExpandedRowKeys | 默认展开的行 key 数组,动态加载数据时不生效。 | (string | number)[] | [] |
| direction | 文字方向,默认值取 ConfigProvider 的 direction,可在此单独配置。 | Direction | - |
| empty | 无数据时展示的内容,默认为语言包中的空状态文案。 | ReactNode | - |
| expandAllGroupRows | 是否展开所有分组行。 | boolean | false |
| expandAllRows | 是否展开所有行。 | boolean | false |
| expandCellFixed | 展开按钮所在列是否固定,取值与 Column 的 fixed 相同。 | Fixed | false |
| expandedRowKeys | 展开的行 key 数组,传入后行展开受控。 | (string | number)[] | - |
| expandedRowRender | 额外的展开行。返回元素,或返回 { children, fixed } 形式的对象设置展开行的固定方式。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。 | TypeExpandedRowRender<RecordType> | - |
| expandIcon | 传 false 隐藏展开按钮;传元素或函数替换展开图标,函数接收 expanded。 | ExpandIcon | - |
| expandRowByClick | 点击行时是否展开可展开行。 | boolean | false |
| footer | 表格尾部,可以是元素,或接收当前页数据返回元素的函数。 | TypeFooter<RecordType> | - |
| getVirtualizedListRef | 虚拟化列表创建后调用,参数为列表的 ref 对象,其 current 上提供 scrollTo 与 scrollToItem。仅在配置 virtualized 时有效。 | GetVirtualizedListRef | - |
| groupBy | 分组依据,可以是数据项中的字段名,或返回字符串、数字的函数。 | TypeGroupBy<RecordType> | - |
| headerStyle | 所有表头单元格(包括固定表头)的内联样式。 | React.CSSProperties | - |
| hideExpandedColumn | 表格可展开时,展开按钮默认与第一列内容渲染在同一个单元格;设为 false 时展开按钮单独占一列。 | boolean | true |
| id | 最外层容器的 id。 | string | - |
| indentSize | 树形数据每一层的缩进宽度,单位为像素。 | number | 20 |
| keepDOM | 折叠行时是否保留被折叠的 DOM。 | boolean | - |
| loading | 是否显示加载状态。 | boolean | false |
| onChange | 分页、排序、筛选变化时触发,参数 changeInfo 包含 pagination、filters、sorter 与 extra,extra.changeType 为 sorter、filter 或 pagination。 | TypeOnChange<RecordType> | - |
| onExpand | 点击行展开图标时触发,参数为是否展开、该行数据与鼠标事件。 | TypeOnExpand<RecordType> | - |
| onExpandedRowsChange | 展开的行变化时触发,参数为展开行的数据数组。 | TypeOnExpandedRowsChange<RecordType> | - |
| onGroupedRow | 类似于 onRow,单独用于分组行的属性与事件,返回的对象会合并到分组行的 tr 上。index 在未开启 virtualized 时是分组序号,开启后是该行在虚拟列表中的位置。 | TypeOnGroupedRow<RecordType> | - |
| onHeaderRow | 设置表头行的属性与事件,返回的对象会合并到表头行的 tr 上(不含 style,请用 headerStyle)。 | TypeOnHeaderRow<RecordType> | - |
| onRow | 设置数据行的属性与事件,第三个参数为行状态(disabled 与 selected)。返回的对象会合并到数据行的 tr 上。 | TypeOnRow<RecordType> | - |
| onRowContextMenu | 数据行触发右键菜单时的回调,先于 onRow 返回的 onContextMenu 调用,两者都会执行。 | Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void | - |
| onRowDoubleClick | 双击数据行时的回调,先于 onRow 返回的 onDoubleClick 调用,两者都会执行。 | Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void | - |
| onRowMouseEnter | 鼠标进入数据行时的回调,先于 onRow 返回的 onMouseEnter 调用,两者都会执行。 | Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void | - |
| onRowMouseLeave | 鼠标离开数据行时的回调,先于 onRow 返回的 onMouseLeave 调用,两者都会执行。 | Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void | - |
| pagination | 分页配置,传 false 关闭分页。 | TablePagination | true |
| prefixCls | 样式名前缀。 | string | "semi-table" |
| ref | 表格的句柄,提供 getCurrentPageData 方法,见下方方法。 | TypeReact.Ref<TableHandle<RecordType>> | - |
| renderGroupSection | 渲染分组行的内容。返回元素时作为分组行唯一一列的内容;返回 { children, ...列配置 } 对象时,children 是内容,其余字段作为该列的配置。 | RenderGroupSection | - |
| renderPagination | 自定义分页器渲染方法,接收分页配置并返回元素。 | RenderPagination | - |
| resizable | 是否开启列宽伸缩,需要伸缩的列必须提供 width。传入对象时可配置 onResize、onResizeStart、onResizeStop 与 handlerClassName。 | TypeResizable<RecordType> | - |
| rowExpandable | 判断某一行是否可展开,返回 false 时不渲染该行的展开按钮。 | TypeRowExpandable<RecordType> | - |
| rowKey | 行 key 的取值:数据项的字段名,或返回字符串、数字的函数。 | TypeRowKey<RecordType> | "key" |
| rowSelection | 行选择配置,传 true 使用默认配置,详见 rowSelection。 | TypeRowSelection<RecordType> | null |
| rowSpanHover | 悬停在带 rowSpan 的单元格上时,高亮它跨越的所有行。 | boolean | - |
| scroll | 滚动区域配置,x 为横向宽度,y 为纵向高度,详见 scroll。 | Scroll | - |
| showHeader | 是否显示表头。 | boolean | true |
| size | 表格尺寸,影响单元格的内边距。 | Size | "default" |
| sticky | 将表头固定在页面顶部。传对象时 top 为距离滚动容器顶部的距离。 | Sticky | - |
| style | 最外层容器的样式。 | React.CSSProperties | - |
| title | 表格标题,可以是元素,或接收当前页数据返回元素的函数。 | TypeTitle<RecordType> | - |
| virtualized | 开启虚拟化,需同时传 scroll.y(数字)。对象形式可配置行高 itemSize、滚动回调 onScroll 与额外渲染的行数 overscanCount。 | Virtualized | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| getCurrentPageData | 返回当前页的数据(深拷贝),形如 { dataSource, groups };groups 是 Map,键为分组键,值为该组的行键集合。 | Type() => Pick<BasePageData<RecordType>, 'dataSource' | 'groups'> |
aria-label、aria-labelledby、aria-describedby 落在 role="table" 的元素上,其他原生属性(如 data-*、onClick)落在最外层的 div 上。
virtualized 传对象时的字段:
| 字段 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| itemSize | 每行的高度,不传时按 size 取默认值:default 为 53,middle 为 45,small 为 37,组头行同普通行 | number | ((index: number, row: { sectionRow?: boolean, expandedRow?: boolean }) => number) | 53 |
| onScroll | 虚拟化滚动回调方法,只在纵向滚动偏移变化时调用 | (object: { scrollDirection?: 'forward' | 'backward', scrollOffset?: number, scrollUpdateWasRequested?: boolean }) => void | - |
| overscanCount | 可视区上下额外渲染的行数 | number | 2 |
headerStyle 示例
headerStyle 会应用到所有表头 <th> 元素,包括 fixed 表头。
import React from 'react';
import { Table } from '@fibrous/semi-ui-react';
const columns = [
{
title: '姓名',
dataIndex: 'name',
width: 180,
fixed: 'left',
},
{
title: '年龄',
dataIndex: 'age',
width: 120,
},
{
title: '地址',
dataIndex: 'address',
width: 280,
},
{
title: '公司',
dataIndex: 'company',
width: 280,
},
];
const dataSource = Array.from({ length: 6 }).map((_, i) => ({
key: i,
name: `Edward ${i}`,
age: 20 + i,
address: '西湖区湖底公园 1 号',
company: 'Semi Design',
}));
export default function Demo() {
return <Table columns={columns} dataSource={dataSource} scroll={{ x: 900 }} headerStyle={{ backgroundColor: '#F5F6F7', fontWeight: 600 }} />;
}
一些上面用到的类型定义:
// PaginationProps 为 Pagination 组件支持的 props
interface TablePaginationProps extends PaginationProps {
position?: PaginationPosition;
formatPageText?: FormatPageText;
}
type VirtualizedItemSizeFn = (index?: number, row?: { sectionRow?: boolean; expandedRow?: boolean }) => number;
type VirtualizedOnScrollArgs = {
scrollDirection?: 'forward' | 'backward';
scrollOffset?: number;
scrollUpdateWasRequested?: boolean;
};
type VirtualizedOnScroll = (object: VirtualizedOnScrollArgs) => void;
type Virtualized =
| boolean
| {
itemSize?: number | VirtualizedItemSizeFn;
onScroll?: VirtualizedOnScroll;
};
RecordType 为 Table 和 Column 的泛型参数,默认为 object 类型。你可以这样使用 RecordType:
import { ColumnProps } from 'table/interface';
interface Record {
title?: string;
dataIndex?: string;
width?: number;
render?: Function;
key?: string;
name?: string;
age?: number;
address?: string;
}
function App() {
const columns: ColumnProps<Record>[] = [
{
title: 'Name',
dataIndex: 'name',
width: 200,
},
// ...
];
const data: Record[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park, New York No. 1 Lake Park',
},
// ...
];
return (
<Table<Record>
columns={columns}
dataSource={data}
// ...
>
);
}
onHeaderRow / onRow 用法
onHeaderRow 中可以返回 th 支持的属性或者事件 onRow 中可以返回 tr 支持的属性或者事件
onRow 的第三个参数 rowStatus 可以获取当前行的状态信息,包括 disabled 和 selected 属性。这在需要根据行的选中或禁用状态执行不同逻辑时非常有用,例如点击行时判断是否允许选中。
import React from 'react';
import { Table } from '@fibrous/semi-ui-react';
() => (
<Table
onRow={(record, index, rowStatus) => {
return {
onClick: (event) => {
// rowStatus.disabled 表示当前行是否被禁用
// rowStatus.selected 表示当前行是否被选中
if (rowStatus?.disabled) {
console.log('该行已被禁用');
return;
}
console.log('点击行', index, rowStatus);
}, // 点击行
onMouseEnter: (event) => {}, // 鼠标移入行
onMouseLeave: (event) => {}, // 鼠标移出行
className: '',
// ...
// 其他可以作用于 tr 的属性或事件
};
}}
onHeaderRow={(columns, index) => {
return {
onClick: (event) => {}, // 点击表头行
onMouseEnter: (event) => {}, // 鼠标移入表头行
onMouseLeave: (event) => {}, // 鼠标移出表头行
className: '',
// ...
// 其他可以作用于 th 的属性或事件
};
}}
/>
);
Column
列配置只接受下表的属性,拼错的属性名会报类型错误。需要在列上携带自定义字段时,用交叉类型扩展,如 ColumnProps<RecordType> & { tooltip: string }。
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | 列内容的对齐方式,可选 left、right 或 center,RTL 时自动切换。未设置时为 left。 | Align | - |
| children | 表头合并时的子列配置。 | TypeArray<ColumnProps<RecordType>> | - |
| className | 列样式名。 | string | - |
| colSpan | 表头单元格横跨的列数,设置为 0 时不渲染该表头单元格。 | number | - |
| dataIndex | 列数据在数据项中对应的字段。使用 sorter 或 onFilter 时必须提供唯一的 dataIndex。 | string | - |
| defaultFilteredValue | 筛选的默认值,值为已筛选的 value 数组,非受控时作为初始筛选状态。 | any[] | - |
| defaultSortOrder | 排序的默认方向,可选 ascend、descend 或 false,非受控时作为初始排序状态。 | SortOrder | - |
| ellipsis | 文本超出单元格时省略显示,开启后表格布局自动切换为 fixed。传对象时,showTitle 设为 false 可隐藏原生 HTML title 提示,配合 render 可自定义内容提示。 | BaseEllipsis | - |
| filterChildrenRecord | 树形表格中筛选时是否同时筛选子数据。开启后,子数据符合条件时父数据即使不符合也会保留。 | boolean | - |
| filterConfirmMode | 筛选确认模式。immediate 为点击筛选项立即生效;confirm 为点击筛选项后需点击确定按钮才生效,此时下拉面板底部会显示确定和重置按钮。未设置时为 immediate。 | FilterConfirmMode | - |
| filterDropdown | 传入元素替换默认的筛选菜单,菜单内的交互由使用方自行编写。 | ColumnFilterProps['filterDropdown'] | - |
| filterDropdownProps | 透传给 Dropdown 的属性,详见 Dropdown API。 | ColumnFilterProps['filterDropdownProps'] | - |
| filterDropdownVisible | 控制筛选 Dropdown 的 visible,详见 Dropdown API。 | boolean | - |
| filteredValue | 筛选的受控值,值为已筛选的 value 数组。传入后筛选状态由使用方控制。 | any[] | - |
| filterIcon | 自定义筛选图标。传元素,或接收 filtered 并返回节点的函数。 | FilterIcon | - |
| filterMultiple | 是否允许多选筛选项,未设置时允许多选。 | boolean | - |
| filters | 表头的筛选菜单项。使用 renderFilterDropdown 时可以不传。 | Filter[] | - |
| fixed | 列是否固定,可选 true(等效于 left)、left 或 right,RTL 时自动切换。 | Fixed | - |
| key | 列的标识,用于选择、排序、筛选与列宽;已设置唯一的 dataIndex 时可以省略。 | string | number | - |
| onCell | 设置数据单元格的属性与事件,参数为数据项与行索引,返回的对象会合并到 td 上。 | TypeOnCell<RecordType> | - |
| onFilter | 本地模式下的筛选函数,接收筛选值与数据项,返回 true 时保留该数据项。使用时必须提供唯一的 dataIndex。 | TypeOnFilter<RecordType> | - |
| onFilterDropdownVisibleChange | 筛选下拉菜单显示状态变化时的回调。 | OnFilterDropdownVisibleChange | - |
| onHeaderCell | 设置表头单元格的属性与事件,参数为列、列索引与表头行的序号,返回的对象会合并到 th 上。 | TypeOnHeaderCell<RecordType> | - |
| render | 自定义单元格内容,参数为 (text, record, index, options)。返回节点,或返回 { children, props } 对象以合并单元格属性(colSpan 为 0 时不渲染该单元格)。options 包含 expandIcon、selection、indentText 与 isHovering。 | TypeColumnRender<RecordType> | - |
| renderFilterDropdown | 自定义筛选下拉菜单的内容。参数包含临时筛选值、设置方法,以及 confirm、clear、close 与 reset 方法,详见自定义筛选器。 | ColumnFilterProps['renderFilterDropdown'] | - |
| renderFilterDropdownItem | 自定义筛选菜单的每一项,参数包含 text、value、checked、onChange、level、filteredValue 与 filterMultiple。 | RenderFilterDropdownItem | - |
| resize | 表格开启 resizable 时,传 false 让该列没有伸缩手柄。 | boolean | - |
| shouldCellUpdate | 自定义单元格是否需要更新,返回 true 时渲染单元格。默认对单元格的 props 做深对比;数据项较复杂时,可由此接管渲染判断。 | Type(props: TableCellProps, prevProps: TableCellProps) => boolean | - |
| showSortTip | 是否展示排序提示,未设置时不展示。设置了 sortOrder 时不生效。 | boolean | - |
| sortChildrenRecord | 树形表格中排序时是否同时排序子数据。 | boolean | - |
| sorter | 排序函数,参数为两个数据项与当前排序方向,返回负数、0 或正数,与 Array.sort 的比较函数相同。需要服务端排序时设为 true。本地排序时必须提供唯一的 dataIndex,且 dataSource 中每条数据项需有独立的 key。 | TypeSorter<RecordType> | - |
| sortIcon | 自定义排序按钮,接收 sortOrder 并返回节点。返回的节点替换整个排序按钮(包含升序和降序),需根据 sortOrder 控制高亮。 | SortIcon | - |
| sortOrder | 排序的受控方向,可选 ascend、descend 或 false。传入后排序状态由使用方控制。 | SortOrder | - |
| title | 列头内容,可以是节点,或接收 { filter, sorter, selection } 的函数。未开启 useFullRender 时函数以空对象调用,返回值与排序、筛选按钮组合;开启后整个列头由函数的返回值决定。 | ColumnTitle | - |
| useFullRender | 是否完全自定义渲染,开启后可在 title 与 render 中读取 sorter、filter、selection 与 expandIcon 等节点,会带来一定的性能损耗。 | boolean | - |
| width | 列宽,数字为像素。使用 resizable 时需要提供。 | string | number | - |
一些上面用到的类型定义:
type Filter = {
value: any;
text: React.ReactNode;
children?: Filter[];
};
Column.onCell / onHeaderCell 用法
与 onRow、onHeaderRow类似,在 column.onCell column.onHeaderCell 中也能返回 td/th 支持的属性或事件
rowSelection
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| className | 所处列样式名 | string | |
| checkRelation | 父子节点选择关联模式。设置为 'related' 时,选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选);默认为 'unRelated' 即父子节点选择互不影响 | 'related' | 'unRelated' | 'unRelated' |
| clickRow | 是否启用点击行选择功能。开启后,点击行任意位置(包括固定列)都会触发行选择/取消选择。被禁用的行(通过 getCheckboxProps 设置)无法通过点击选中;disabled 为 true 时点击行也不会选中。 | boolean | false |
| defaultSelectedRowKeys | 初始选中项的 key 数组,只在挂载时读取,传了 selectedRowKeys 时不生效 | (string | number)[] | |
| disabled | 表头的 Checkbox 是否禁用;为 true 时 clickRow 点击行选择也不生效 | boolean | false |
| fixed | 把选择框列固定在左边 | boolean | false |
| getCheckboxProps | 选择框的默认属性配置 | (record: RecordType) => object | |
| hidden | 是否隐藏选择列 | boolean | false |
| renderCell | 自定义渲染勾选框 | ({ selected: boolean, record: RecordType, originNode: JSX.Element, inHeader: boolean, disabled: boolean, indeterminate: boolean, index?: number, selectRow?: (selected: boolean, e: Event) => void, selectAll?: (selected: boolean, e: Event) => void }) => ReactNode | |
| selectedRowKeys | 指定选中项的 key 数组,需要和 onChange 进行配合 | string[] | |
| shouldCellUpdate | 自定义控制单元格是否渲染。默认 cell 会深对比 props 和 nextProps 是否变化,来决定是否渲染单元格。如果你的 props 中的 record 比较复杂,建议使用 shouldCellUpdate 接管单元格的渲染。 | (props: TableCellProps, prevProps: TableCellProps) => boolean | |
| width | 自定义列表选择框宽度 | string|number | |
| onChange | 选中项发生变化时的回调。第一个参数会保存上次选中的 row keys,即使你做了分页受控或更新了 dataSource FAQ | (selectedRowKeys: number[]|string[], selectedRows: RecordType[]) => void | |
| onHeaderCell | 设置头部单元格属性,index 为表头行的序号 | (column: Column, columnIndex: number, index: number) => object | |
| onSelect | 用户手动选择某行的回调。nativeEvent 为选择框的 change 事件;clickRow 点击行时为鼠标事件;经 renderCell 的 selectRow 触发时为传给它的事件 | (record: RecordType, selected: boolean, selectedRows: RecordType[], nativeEvent: CheckboxEvent | React.MouseEvent | Event) => void | |
| onSelectAll | 用户手动点击表头选择框的回调,会选中/取消选中当前筛选结果里的所有可选行(不含禁用行),changedRows 只包含选中状态发生变化的行。e 为选择框的 change 事件,经 renderCell 的 selectAll 触发时为传给它的事件 | (selected: boolean, selectedRows: RecordType[], changedRows: RecordType[], e: CheckboxEvent | Event) => void |
scroll
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| scrollToFirstRowOnChange | 当分页、排序、筛选变化后是否自动滚动到表格顶部。当设置了 scroll.y 时,会重置表格内部滚动位置到顶部;当未设置 scroll.y 时,会滚动页面到表格头部位置 | boolean | false |
| x | 设置横向滚动区域的宽,可以为像素值、百分比或 'max-content' | string|number | |
| y | 设置纵向滚动区域的高,可以为像素值 | number |
pagination
翻页组件配置。pagination 建议不要使用字面量写法。
注意:pagination.onChange 设置后,Table onChange 不再响应分页器变化。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| currentPage | 当前页码 | number | - |
| defaultCurrentPage | 默认的当前页码 | number | 1 |
| formatPageText | 翻页区域文案自定义格式化,传 false 关闭文案显示;该项影响表格翻页区域左侧文案显示,不同于 Pagination 组件的 showTotal 参数,请注意甄别。 | boolean | ({ currentStart: number, currentEnd: number, total: number }) => string|ReactNode | true |
| pageSize | 每页条数 | number | 10 |
| position | 位置 | 'bottom'|'top'|'both' | 'bottom' |
| total | 数据总数 | number | 0 |
| preventPageChangeOnPageSizeChange | 切换 pageSize 时是否阻止自动调整 currentPage。默认情况下,切换 pageSize 时组件会自动计算新的 currentPage 以保持当前数据位置,设为 true 后由用户自行控制页码变化 | boolean | false |
其他配置详见Pagination
Resizable
resizable 对象型的参数,主要包括一些表格列伸缩时的事件方法。这些事件方法都可以返回一个对象,该对象会和最终的 column 合并。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| handlerClassName | 列宽调整期间加到该列上的 class 名 | string | 'resizing' |
| onResize | 表格列改变宽度时触发 | (column: Column) => Column | void | |
| onResizeStart | 表格列开始改变宽度时触发 | (column: Column) => Column | void | |
| onResizeStop | 表格列停止改变宽度时触发 | (column: Column) => Column | void |
方法
通过 ref 可以访问到 Table 提供的一些内部方法:
import React, { useRef, useEffect } from 'react';
import { Table } from '@fibrous/semi-ui-react';
function Demo() {
const ref = useRef();
useEffect(() => {
ref.current.getCurrentPageData(); // => { dataSource: [/*...*/], groups: /*...*/ }
}, []);
return <Table columns={[/*...*/]} dataSource={[/*...*/]} ref={ref} />;
}
句柄上的方法见 Table API 中的方法表。
Accessibility
ARIA
Table 按 WAI-ARIA Table 输出静态表格语义,单元格之间不用方向键移动,可交互内容(排序、筛选、展开、选择)都是可以 Tab 聚焦的原生控件。
- 表格的 role 为 table,行为 row,表头单元格为 columnheader,单元格为 cell;树形表格同样是 table,不输出行的层级
- 排序按钮是表头里的原生
button,以列标题命名;当前排序方向写在表头单元格的aria-sort上(ascending或descending),未排序的列不带该属性。showSortTip的提示在按钮聚焦时显示 - 展开按钮是原生
button,名称为「展开此行」,aria-expanded表示该行是否展开 - 行选择的复选框带有「选择此行」「全选所有行」等可访问名称
- 默认的列筛选图标是原生
button,可用 Tab 聚焦,Enter、Space 或下箭头打开筛选菜单,并带有aria-haspopup="menu"与aria-expanded;自定义filterIcon时由使用方保证其可聚焦
- 固定表头(
scroll.y、sticky)或虚拟化时,表头和表体渲染为两个 table 元素,二者的共同容器是 role 为 table 的元素,内部 table 为 role none,单元格仍与列头关联 - 虚拟化表格的
aria-rowcount为表头行数加数据总行数,数据行的aria-rowindex接在表头行之后,屏幕阅读器据此报出不在 DOM 中的行
已知限制:
- 带排序的列,
title渲染在排序按钮里,title中不要放按钮、链接等可交互内容
RTL/LTR
- Table 的 RTL 默认值为 ConfigProvider direction,可以通过 Table direction 覆盖
- Table 列的 align 与 fixed 属性会在 RTL 时会自动切换,left <-> right
- Table 的树形数据暂不支持 RTL(Chrome、Safari 浏览器表现与 Firefox 表现不同)
文案规范
- 表格标题
- 表格标题应清晰的让用户感知到表格的目的;
- 为复杂表格添加描述,为用户提供更多关于表格的上下文信息;
- 使用句子大小写;
- 列标题
- 保持列标题简洁,建议使用 1-2 个词作为列标题;
- 当列标题较长时,建议 2 行显示,剩余文字缩略并在 Tooltip 中显示完全;
- 采用 Sentence case 的大小写规则;
- 列标题使用句子大小写;
- 表格操作
- 可以遵循 Button 的文案规范
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration-table_body-bg | var(--semi-transition_duration-none) | 表格-背景颜色-动画持续时间 |
| $transition_function-table_body-bg | var(--semi-transition_function-easeOut) | 表格-背景颜色-过渡曲线 |
| $transition_delay-table_body-bg | 0ms | 表格-背景颜色-延迟时间 |
| $transition_duration-table_row-head-bg | var(--semi-transition_duration-none) | 表格-行头-背景颜色-动画持续时间 |
| $transition_function-table_row-head-bg | linear | 表格-行头-背景颜色-过渡曲线 |
| $spacing-table-paddingY | $spacing-base | 表格空数据占位及虚拟列表分组垂直内边距 |
| $spacing-table-paddingX | $spacing-base-tight | 表格空数据占位及虚拟列表分组水平内边距 |
| $spacing-table_middle-paddingY | $spacing-base-tight | 中尺寸表格单元格垂直内边距 |
| $spacing-table_small-paddingY | $spacing-tight | 小尺寸表格单元格垂直内边距 |
| $spacing-table_title-paddingY | $spacing-base | 表格标题垂直内边距 |
| $spacing-table_title-paddingX | 0 | 表格标题水平内边距 |
| $spacing-table_footer-padding | $spacing-base | 表格 footer 内边距 |
| $spacing-table_expand_row-paddingLeft | 16px | 表格展开行左侧内边距 |
| $spacing-table_expand_row-paddingRight | 16px | 表格展开行右侧内边距 |
| $spacing-table_resizable-offset-y | 4px | 可拖动拉伸操作垂直偏移量 |
| $spacing-table_resizable-bottom | 4px | 可拖动拉伸操作底部距离 |
| $spacing-table_row_head-paddingY | $spacing-base-tight | 表头垂直内边距 |
| $spacing-table_row_head-paddingX | $spacing-base | 表头水平内边距 |
| $spacing-table_react_resizable_handle-right | -1px | 可拖动拉伸操作向右偏移量 |
| $spacing-table_tbody_rowCell-padding | $spacing-base | 表格体单元格内边距(默认尺寸) |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingX | 16px | 表格选择状态行(非复选框列)单元格水平内边距 |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingY | 10px | 表格选择状态行(非复选框列)垂直内边距 |
| $spacing-table_column_sorter-marginLeft | $spacing-extra-tight | 表头标题与排序按钮间距 |
| $spacing-table_column_filter-marginLeft | $spacing-extra-tight | 列过滤器按钮左侧外边距 |
| $spacing-table_bordered_titler-paddingLeft | $spacing-base | 带边框表格标题左侧内边距 |
| $spacing-table_bordered_titler-paddingRight | $spacing-base | 带边框表格标题右侧内边距 |
| $spacing-table_expand_icon-marginRight | 8px | 行展开按钮右侧外边距 |
| $spacing-table_panel_operation-paddingX | $spacing-base | 表格操作栏水平内边距 |
| $spacing-table_panel_operation-paddingY | $spacing-tight | 表格操作栏垂直内边距 |
| $width-table_base_border | 1px | 表格单元格分割线宽度 |
| $width-table_header_border | 1px | 表格表头分割线宽度 |
| $width-table_resizer_border | 2px | 表格拉伸列标示线宽度 |
| $width-table_column_selection | 48px | 表格选择列及展开列宽度 |
| $width-table_column_sorter-icon | 16px | 表格排序按钮宽度 |
| $height-table_column_sorter-icon | 16px | 表格排序按钮高度 |
| $width-table_column_filter-icon | 16px | 表格过滤按钮宽度 |
| $height-table_column_filter-icon | 16px | 表格过滤按钮高度 |
| $width-table_cell_fixed_left_last | 1px | 表格左侧固定列右侧分割线宽度 |
| $width-table_cell_fixed_right_first | 1px | 表格右侧固定列左侧分割线宽度 |
| $width-table_react_resizable_handle | 9px | 表格伸缩列调节热区宽度 |
| $height-table_pagination_outer_min | 60px | 表格分页器高度 |
| $height-table_column_filter_dropdown | 290px | 表格过滤筛选列表最大高度 |
| $color-table_panel-bg-default | var(--semi-color-primary) | 操作区域样式默认背景颜色 |
| $color-table_panel-text-default | var(--semi-color-primary-light-active) | 表格操作栏选中项文本颜色 |
| $color-table-bg-default | var(--semi-color-bg-1) | 表格固定列及吸顶表头背景颜色 |
| $color-table-text-default | var(--semi-color-text-0) | 单元格默认文字颜色 |
| $color-table-border-default | var(--semi-color-border) | 表格描边颜色 |
| $color-table_shadow-bg-default | rgb(from var(--semi-black) r g b / 0.08) | 表格滚动后阴影颜色 |
| $color-table_shadow-bg-dark | rgb(from var(--semi-black) r g b / 0.4) | 表格滚动后阴影颜色 - 暗色模式 |
| $color-table_shadow-border-default | var(--semi-color-border) | 表格拟阴影 描边颜色 |
| $color-table_th-bg-default | var(--semi-color-bg-1) | 表头背景底色,须不透明以遮住滚动到固定列和吸顶表头下的内容 |
| $color-table_th-fill-default | var(--semi-color-fill-0) | 表头背景叠加的填充色 |
| $color-table_th-border-default | var(--semi-color-border) | 表头底部分割线颜色 |
| $color-table_th-text-default | var(--semi-color-text-2) | 表头文字颜色 |
| $color-table_th-clickSort-bg-hover | var(--semi-color-fill-1) | 点击表头触发排序背景色 - 悬浮 |
| $color-table_pl-bg-default | transparent | |
| $color-table_body-bg-default | var(--semi-color-bg-1) | 表格行背景颜色 - 默认态 |
| $color-table_body-bg-hover | var(--semi-color-fill-0) | 表格行背景颜色 - 悬浮态 |
| $color-table_footer-bg-default | var(--semi-color-fill-0) | 表格 footer 背景颜色 - 默认 |
| $color-table_row_expanded-bg-default | var(--semi-color-fill-0) | 表格展开行背景颜色 - 默认 |
| $color-table_expanded_icon-default | var(--semi-color-text-2) | 表格展开行图标颜色 - 默认 |
| $color-table_expanded-bg-default | transparent | 表格展开行图标容器背景颜色 - 默认 |
| $color-table_filter_on-text-default | var(--semi-color-primary) | 表格过滤按钮颜色 - 激活态 |
| $color-table_filter-text-default | var(--semi-color-text-2) | 表格过滤按钮颜色 - 默认态 |
| $color-table_sorter_on-text-default | var(--semi-color-primary) | 表格排序按钮颜色 - 激活态 |
| $color-table_sorter-text-default | var(--semi-color-text-2) | 表格排序按钮颜色 - 默认态 |
| $color-table_sorter-text-hover | var(--semi-color-text-2) | 表格排序按钮颜色 - 悬浮态 |
| $color-table_page-text-default | var(--semi-color-text-2) | 表格翻页器文本颜色 |
| $color-table_resizer-bg-default | var(--semi-color-primary) | 表格拉伸标示线颜色 |
| $color-table_selection-bg-default | var(--semi-grey-0) | 表格分组背景色 |
| $color-table_placeholder-text-default | var(--semi-color-text-2) | 表格空数据文本颜色 |
| $color-table_cell-bg-hover | var(--semi-color-bg-0) | 让表格在 hover 时正确显示 $color-table_body-bg-hover 颜色,如无必要不要修改 |
| $font-table_base-fontSize | 14px | 表格默认文本字号 |
| $border-table_base-borderStyle | solid | 表格描边样式 |
| $width-table_fixed_shadow | 6px | 表格固定列滚动阴影宽度 |
| $z-table_fixed_column | $z-table-fixed | fixed列的zIndex值 |
FAQ
-
点击第二页的行选择按钮,会跳转到第一页?
Table 的 dataSource 更新后,会将页码重置到初始态。请检查数据源是否在组件渲染时发生了变化。
function App() { const [dataSource, setDataSource] = useState([]); useEffect(() => { // ✅ 正确 const getData = () => { // 获取数据 const newData = fetch(/**/); // 设置数据 setDataSource(dataSource); }; getData(); }, []); // ❌ 错误 const data = []; return <Table dataSource={data} columns={[/*...*/]} />; } -
筛选后的数据条数不对?
请检查你的筛选列和数据源是否配置正确。
筛选列需设置独立的 dataIndex,同时 dataSource 需要设置独立的 key,请参考 dataSource API。否则筛选功能无法正常工作。
-
表格数据为何没有更新?
Table 组件目前所有参数都为浅层对比,也就是说如果该参数值类型为一个 Array 或者 Object,你需要手动改变其引用才能触发更新。同理,如果你不想触发额外更新,尽量不要直接在传参的时候使用字面量或是在 render 过程中定义引用型参数值:
// ...render() { <Table dataSource={[/*...*/]} columns={[/*...*/]} />}上述的写法在每次 render 时都会触发表格内部对数据的更新(会清空当前的选中行以及展开行 key 数组等)。为了性能及避免一些异常,请尽量将一些引用型参数定义在 render 方法之外(如果使用了 hooks 请利用 useMemo 或者 useState 进行存储)。
-
为何我的表格行不能选中以及展开?
请指定 rowKey 或者给 dataSource 的每项设置一个各不相同的 "key" 属性。表格内所有行相关的操作都需要使用到。
-
如何实现点击排序按钮时自定义排序或传参给服务端排序?
onChange 方法的入参包括 pagination、filters、sorter,用户可以根据 sorter 对 dataSource 进行自定义排序。
-
如何给某一行添加 className?
使用 onRow 或 onHeaderRow。
-
如何给 table cell 设置样式?
涉及到单个 cell 需要控制样式的,可以通过 column.onHeaderCell、column.onCell 控制。
-
为何 rowSelection onChange 的第一个参数缓存了之前选中的 keys?
这么做为了在分页受控时,在第一页选中数据后,去第二页选择数据,回到第一页后选择的 row keys 丢失的场景。如果不想用缓存的 keys,可以从当前 dataSource 过滤一遍,或者使用 rowSelection onChange 的第二个参数。
-
支持单行选择吗
Table 暂不支持单行选择功能,用户可以通过自定义方式实现单选。