如何使用
往 Table 传入表头 columns 和数据 dataSource 进行渲染。
import { Table } from '@fibrous/semi-ui-solid';
function App() {
const columns = [
{
title: '标题',
dataIndex: 'name',
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
];
const data = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
},
{
key: '3',
name: '设计文档',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
},
];
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-solid';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
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',
},
];
export default function TableDemo() {
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
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 { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type AvatarColor } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: string;
updateTime: string;
avatarBg: AvatarColor;
}
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={{ 'margin-right': '12px' }} />
{text}
</div>
);
};
const renderOwner = (text: string, record: Row) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
export default function TableDemo() {
return (
<Table dataSource={data} pagination={false}>
<Table.Column title="标题" dataIndex="name" key="name" render={renderName} />
<Table.Column title="大小" dataIndex="size" key="size" />
<Table.Column title="所有者" dataIndex="owner" key="owner" render={renderOwner} />
<Table.Column title="更新时间" dataIndex="updateTime" key="updateTime" />
<Table.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 { IconTickCircle, IconComment, IconClear, IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
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',
},
{
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 columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
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 = { pageSize: 3 };
export default function TableDemo() {
return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={pagination} />;
}自定义渲染
用户可以使用 Column.render 来自定义某一列单元格的渲染,该功能适用于需要渲染较为复杂的单元格内容时。render 每次行数据或序号变化时重新调用,返回的 JSX 中读取 options 的字段即可跟随行状态更新,无需额外订阅。
render 函数的第四个参数 options 是一个对象,包含以下属性:
expandIcon: 展开图标的渲染函数(当使用树形数据或可展开行时),在插入处调用,如{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 { createSignal } from 'solid-js';
import { IconTickCircle, IconComment, IconClear, IconDelete } from '@fibrous/semi-icons-solid';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrous/semi-illustrations-solid';
import { Table, Avatar, Button, Empty, Typography, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
const { Text } = Typography;
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue' | 'green';
}
const raw: Row[] = [
{
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, setDataSource] = createSignal<Row[]>(raw);
const removeRecord = (key: string) => {
setDataSource((list) => list.filter((item) => item.key !== key));
};
const resetData = () => {
setDataSource([...raw]);
};
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<span style={{ display: 'flex', 'align-items': 'center' }}>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{/* 宽度 = 单元格设定宽度 − 非文本内容宽度 */}
<Text ellipsis={{ showTooltip: true }} style={{ width: 'calc(400px - 76px)' }}>
{text}
</Text>
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
width: 150,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
width: 300,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{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)} />,
},
];
return (
<>
<Button onClick={resetData} style={{ 'margin-bottom': '10px' }}>
重置
</Button>
<Table style={{ 'min-height': '350px' }} columns={columns} dataSource={dataSource()} pagination={false} empty={<Empty image={<IllustrationNoResult />} darkModeImage={<IllustrationNoResultDark />} description="搜索无结果" />} />
</>
);
}带分页组件的表格
表格分页目前支持两种模式:受控和非受控模式。
- 受控模式下,分页的状态完全由外部传入,依据为是否往 Table 传入了
pagination.currentPage这个字段。一般情况下,受控模式适用于远程拉取数据并渲染。 - 非受控模式下,Table 默认会将传入的
dataSource长度作为total传给 Pagination 组件,当然你也可以传入一个total字段来覆盖 Table 组件的取值,不过我们并不推荐用户在非受控分页模式下传入这个字段。
标题 | 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 待评审 | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 已交付 | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 待评审 | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 已交付 | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 待评审 | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 已交付 | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 待评审 | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 已交付 | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 待评审 | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: 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 = { y: 300 };
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}拉取远程数据
正常情况下,数据往往不是一次性获取的,我们会在点击页码、过滤器或者排序按钮时从接口重新获取数据,这种情况下请使用受控模式来处理分页。用户需往 Table 传入 pagination.currentPage 这个字段,此时分页组件的渲染完全依赖于传入的 pagination 对象。
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 待评审 | 郝郝宣 | 2024-05-11 | |
| 10 KB | 已交付 | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 待评审 | 郝郝宣 | 2024-05-21 | |
| 20 KB | 已交付 | 姜姜鹏志 | 2024-05-26 |
代码
import { createResource, createSignal } from 'solid-js';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 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 [currentPage, setPage] = createSignal(1);
// createResource drops responses for pages the user has already left
const [pageData] = createResource(currentPage, fetchData);
return (
<Table
columns={columns}
dataSource={pageData() ?? []}
pagination={{
currentPage: currentPage(),
pageSize: 5,
total: data.length,
onPageChange: setPage,
}}
loading={pageData.loading}
/>
);
}固定列或表头
可以通过设置 column 的 fixed 属性以及 scroll.x 来进行列固定,通过设置 scroll.y 来进行表头固定。
如果是固定值,设置为 >=所有固定列宽之和 + 所有表格列宽之和 的数值。
- 建议指定
scroll.x为大于表格宽度的固定值或百分比。如果是固定值,设置为>=所有固定列宽之和+所有表格列宽之和的数值。- 若列头与内容不对齐或出现列重复或者固定列失效的情况,请指定固定列的宽度
width,若指定宽度后仍不生效,请尝试建议留一列不设宽度以适应弹性布局,或者检查是否有超长连续字段破坏布局。- 请确保表格内部的所有元素在渲染后不会对单元格的高度造成影响(例如含有未加载完成的图片等),这种情况下请给定子元素一个确定的高度,以此确保左右固定列单元格不会错乱。
标题 | 所有者 |
|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |||
| 5 KB | 郝郝宣 | 2024-05-11 | |||
| 10 KB | 姜姜鹏志 | 2024-05-16 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |||
| 20 KB | 姜姜鹏志 | 2024-05-26 | |||
| 25 KB | 郝郝宣 | 2024-05-31 | |||
| 30 KB | 姜姜鹏志 | 2024-06-05 | |||
| 35 KB | 郝郝宣 | 2024-06-10 | |||
| 40 KB | 姜姜鹏志 | 2024-06-15 | |||
| 45 KB | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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: () => <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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const scroll = { y: 300, x: 1200 };
const rowSelection: 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,
};
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}通过 sticky 属性可以将表头固定在页面顶部。传入 top 时可以控制距离滚动容器的距离。
开启 sticky 后,Table 会自动打开 fixed 布局,列宽将由 column.width 决定。没有给定 width 的列宽由浏览器自动分配。
带排序和过滤功能的表头
表格内部集成了过滤器和排序控件,用户可以通过在 Column 中传入 filters 以及 onFilter 开启表头的过滤器控件展示,传入 sorter 开启表头的排序控件的展示。
| 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 待评审 | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 已交付 | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 待评审 | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 已交付 | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 待评审 | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 已交付 | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 待评审 | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 已交付 | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 待评审 | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: 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 = { y: 300 };
export default function TableDemo() {
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-solid';
const figmaIconUrl = '/assets/placeholder/app.svg';
const docIconUrl = '/assets/docs-icon.png';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: number | undefined;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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 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',
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} />;
}自定义表头筛选
如果你需要将筛选器输入框展示在表头,可在 title 传入返回 JSX 的函数,配合 filteredValue 使用。filteredValue 可以是 signal 的读取,它变化后表头和数据会同步更新。
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| 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 { createSignal } from 'solid-js';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Input, Space, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
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('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const figmaIconUrl = '/assets/placeholder/app.svg';
export default function TableDemo() {
const [filteredValue, setFilteredValue] = createSignal<string[]>([]);
// The composition flag is read by event handlers only, so it does not need to be reactive
let isComposition = false;
const handleChange = (value: string) => {
if (isComposition) {
return;
}
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const handleCompositionStart = () => {
isComposition = true;
};
const handleCompositionEnd = (event: { currentTarget: HTMLInputElement }) => {
isComposition = false;
const value = event.currentTarget.value;
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const columns: ColumnProps<Row>[] = [
{
title: () => (
<Space>
<span>标题</span>
<Input placeholder="请输入筛选值" style={{ width: '200px' }} 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={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
onFilter: (value, record) => record.name.includes(value),
// Read in a getter so the table follows the signal after the columns are created
get filteredValue() {
return filteredValue();
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{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 自定义渲染筛选器面板。参数的字段是实时的,在返回的 JSX 中读取即可。
你可以在用户输入筛选值的时候调用 setTempFilteredValue 存储筛选值,在筛选值输入完毕后调用 confirm 触发真正的筛选。也可以通过 confirm({ filteredValue }) 直接筛选。
type RenderFilterDropdown = (props: RenderFilterDropdownProps) => JSX.Element;
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 | 姜姜鹏志 | 2024-05-06 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
| 30 KB | 姜姜鹏志 | 2024-06-05 | |
| 40 KB | 姜姜鹏志 | 2024-06-15 | |
| 50 KB | 姜姜鹏志 | 2024-06-25 | |
| 60 KB | 姜姜鹏志 | 2024-07-05 | |
| 70 KB | 姜姜鹏志 | 2024-07-15 | |
| 80 KB | 姜姜鹏志 | 2024-07-25 | |
| 90 KB | 姜姜鹏志 | 2024-08-04 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, Input, Button, Space, type ColumnProps } from '@fibrous/semi-ui-solid';
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 < 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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const figmaIconUrl = '/assets/placeholder/app.svg';
export default function TableDemo() {
let inputEl: HTMLInputElement | undefined;
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdown: (props) => {
const handleChange = (value: string) => {
const filteredValue = value ? [value] : [];
props.setTempFilteredValue(filteredValue);
};
return (
<Space vertical align="start" style={{ padding: '8px' }}>
<Input inputRef={(el) => (inputEl = el)} value={props.tempFilteredValue[0]} onChange={handleChange} />
<Space>
<Button onClick={() => props.confirm({ closeDropdown: true })}>筛选 + 关闭</Button>
<Button onClick={() => props.clear({ closeDropdown: true })}>清除 + 关闭</Button>
<Button onClick={() => props.close()}>直接关闭</Button>
</Space>
</Space>
);
},
onFilterDropdownVisibleChange: () => {
inputEl?.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={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
onFilter: (value, record) => record.owner.includes(value),
defaultFilteredValue: ['姜鹏志'],
renderFilterDropdown: (props) => {
const handleChange = (value: string) => {
if (value) {
props.setTempFilteredValue([value]);
} else {
props.setTempFilteredValue([]);
}
};
return (
<Space vertical align="start" style={{ padding: '8px' }}>
<Input value={props.tempFilteredValue[0]} onChange={handleChange} />
<Space>
<Button onClick={() => props.confirm({ closeDropdown: false })}>筛选后不关闭</Button>
<Button onClick={() => props.clear({ closeDropdown: false })}>清除后不关闭</Button>
<Button onClick={() => props.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 * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
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;
};
const dataSource = 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={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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');
},
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} />;
}自定义筛选项渲染
支持往 column 中传入 renderFilterDropdownItem 自定义每个筛选项的渲染方式,参数如下:
text: string当前筛选项的文案;value: any当前筛选项的值;checked: boolean当前筛选项是否已经选中;filteredValue: any[]当前所有的筛选值;level: number当前筛选项所处层级,如果是嵌套的筛选项,该值会 >= 1;filterMultiple: boolean当前筛选项是否为多选。
| 所有者 |
|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, Dropdown, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdownItem: (itemInfo) => (
<Dropdown.Item onClick={itemInfo.onChange} active={itemInfo.checked}>
{itemInfo.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={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: 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 = { y: 300 };
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}可以展开的表格
一般可展开行
如果需要渲染可以展开的表格,除了需要在 Table 传 expandedRowRender 这个方法外,还必须要指定 rowKey(默认为 key),Table 会根据 rowKey 取得行唯一标识符。展开的行可以通过 expandedRowKeys 与 onExpandedRowsChange 受控。
- 如果
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 { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type DescriptionsData, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <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 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 expandData: DescriptionsData[][] = [
[
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
[
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
[
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
];
const renderValue = (item: DescriptionsData) => (item.key === '垂类标签' ? <Tag style={{ margin: '0px' }}>{item.value}</Tag> : item.value);
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => <Descriptions align="justify" data={expandData[index]} renderValue={renderValue} />;
export default function TableDemo() {
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 { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type DescriptionsData, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <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 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 expandData: DescriptionsData[][] = [
[
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
[
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
[
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
];
const renderValue = (item: DescriptionsData) => (item.key === '垂类标签' ? <Tag style={{ margin: '0px' }}>{item.value}</Tag> : item.value);
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => <Descriptions align="justify" data={expandData[index]} renderValue={renderValue} />;
export default function TableDemo() {
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 { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type DescriptionsData, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <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 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 expandData: DescriptionsData[][] = [
[
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
[
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
[
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
];
const renderValue = (item: DescriptionsData) => (item.key === '垂类标签' ? <Tag style={{ margin: '0px' }}>{item.value}</Tag> : item.value);
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => <Descriptions align="justify" data={expandData[index]} renderValue={renderValue} />;
export default function TableDemo() {
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, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key?: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: Row[];
}
const columns: ColumnProps<Row>[] = [
{
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: Row[] = [
{
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: '无',
},
],
},
];
export default function TableDemo() {
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 { createSignal } from 'solid-js';
import { IconArrowUp, IconArrowDown } from '@fibrous/semi-icons-solid';
import { Table, Button, type ColumnProps } from '@fibrous/semi-ui-solid';
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: '无',
},
],
},
];
// Swaps two sibling records; returns undefined when the keys are not found, so the caller keeps the old tree
const swapSiblings = (list: TreeRow[], key1: number, key2: number) => {
const index1 = list.findIndex((item) => item.key === key1);
const index2 = list.findIndex((item) => item.key === key2);
if (index1 > -1 && index2 > -1) {
const next = [...list];
[next[index1], next[index2]] = [next[index2], next[index1]];
return next;
}
for (let i = 0; i < list.length; i++) {
const children = list[i].children;
const swapped = children && swapSiblings(children, key1, key2);
if (swapped) {
const next = [...list];
next[i] = { ...list[i], children: swapped };
return next;
}
}
return undefined;
};
const getSameLevelRecords = (key: number | undefined, list: TreeRow[]): TreeRow[] => {
if (key != null) {
if (list.some((item) => item.key === key)) {
return list;
}
for (const item of list) {
const records = item.children ? getSameLevelRecords(key, item.children) : [];
if (records.length) {
return records;
}
}
}
return [];
};
export default function TableDemo() {
const [expandedRowKeys, setExpandedRowKeys] = createSignal<number[]>([1, 2]);
const [data, setData] = createSignal<TreeRow[]>(raw);
const switchRecord = (key1: number, key2: number) => {
const swapped = swapSiblings(data(), key1, key2);
if (swapped) {
setData(swapped);
}
};
const siblingKey = (key: number | undefined, offset: number) => {
const records = getSameLevelRecords(key, data());
const index = records.findIndex((item) => item.key === key);
return records[index + offset]?.key;
};
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) => {
// Read through functions so the buttons follow the tree after a swap
const upKey = () => siblingKey(record.key, -1);
const downKey = () => siblingKey(record.key, 1);
return (
<>
<Button
aria-label="上移"
icon={<IconArrowUp />}
disabled={upKey() === undefined}
onClick={() => {
const key = upKey();
if (key !== undefined && record.key !== undefined) switchRecord(record.key, key);
}}
/>
<Button
aria-label="下移"
icon={<IconArrowDown />}
disabled={downKey() === undefined}
onClick={() => {
const key = downKey();
if (key !== undefined && record.key !== undefined) switchRecord(record.key, key);
}}
/>
</>
);
},
},
];
return <Table columns={columns} dataSource={data()} rowKey="key" childrenRecordName="children" 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 { createSignal } from 'solid-js';
import { union, pull } from 'es-toolkit';
import { Table, type ColumnProps } from '@fibrous/semi-ui-solid';
type TreeRow = {
key: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
};
type RowKey = string | number;
const traverse = (data: TreeRow[], res: RowKey[]) => {
for (const record of data) {
res.push(record.key);
if (Array.isArray(record.children)) {
traverse(record.children, res);
}
}
};
const getAllRowKeys = (data: TreeRow[]) => {
const allRowKeys: RowKey[] = [];
traverse(data, allRowKeys);
return allRowKeys;
};
const findShouldSelectRowKeys = (record: TreeRow, selected: boolean, selectedRowKeys: RowKey[]) => {
const childrenRowKeys: RowKey[] = [];
if (Array.isArray(record.children)) {
traverse(record.children, childrenRowKeys);
}
if (!selected) {
return pull([...selectedRowKeys], [record.key, ...childrenRowKeys]);
}
return union(selectedRowKeys, [record.key, ...childrenRowKeys]);
};
const columns: 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: 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: '无',
},
],
},
];
export default function TableDemo() {
const [selectedRowKeys, setSelectedRowKeys] = createSignal<RowKey[]>([]);
const doSelect = (record: TreeRow, selected: boolean) => {
const rowKeys = findShouldSelectRowKeys(record, selected, selectedRowKeys());
setSelectedRowKeys(rowKeys);
console.log('select', record, rowKeys);
};
const doSelectAll = (selected: boolean) => {
setSelectedRowKeys(selected ? getAllRowKeys(data) : []);
};
return (
<Table
columns={columns}
rowKey="key"
childrenRecordName="children"
rowSelection={{
selectedRowKeys: selectedRowKeys(),
onSelect: doSelect,
onSelectAll: doSelectAll,
}}
dataSource={data}
pagination={false}
/>
);
}树形选择关联(checkRelation)
通过设置 rowSelection.checkRelation 为 'related',可以实现父子节点选择关联。选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选)。
| Key | 名称 | 数据类型 | 描述 | |
|---|---|---|---|---|
| videos_info | 视频信息 | Object | 视频的元信息 | |
| text_info | 文本信息 | Object | 文本的元信息 |
代码
import { createSignal } from 'solid-js';
import { Table, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
dataKey: string;
name: string;
type: string;
description: string;
children?: Row[];
}
const columns = [
{
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: Row[] = [
{
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: '文本描述',
},
],
},
];
export default function TableDemo() {
const [selectedRowKeys, setSelectedRowKeys] = createSignal<(string | number)[]>([]);
const rowSelection = (): RowSelectionProps<Row> => ({
selectedRowKeys: selectedRowKeys(),
onChange: (keys = []) => {
setSelectedRowKeys(keys);
},
checkRelation: 'related',
});
return <Table columns={columns} dataSource={data} rowSelection={rowSelection()} pagination={false} />;
}自定义行或单元格事件以及属性
- 传入
onRow/onHeaderRow可以定义表格或表头行的原生事件或属性。 - 传入
column.onCell/column.onHeaderCell可以定义表格或表头单元格原生事件或属性。
原则上 tr/td/th 上支持的属性或事件都能够被定义,返回对象使用 JSX 的属性名:class、style、data-* 与事件 onClick、onMouseEnter、onMouseLeave、onDblClick、onContextMenu 等。例如下面这个例子:
- 表头的 tr 定义了
onMouseEnter/onMouseLeave - 表格的 tr 定义了
class - 表格的第三行定义了
onClick
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-11 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
| 25 KB | 郝郝宣 | 2024-05-31 | |
| 30 KB | 姜姜鹏志 | 2024-06-05 | |
| 35 KB | 郝郝宣 | 2024-06-10 | |
| 40 KB | 姜姜鹏志 | 2024-06-15 | |
| 45 KB | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnHeaderRow, type OnRow } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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: 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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
const onRow: OnRow<Row> = (record, index) => {
if (index === 2) {
return {
class: 'my-tr-class',
onClick: () => console.log('mouse click: ', record, index),
};
}
return {
class: 'my-tr-class',
};
};
const onHeaderRow: OnHeaderRow<Row> = (headerColumns, index) => {
return {
onMouseEnter: () => console.log('mouse enter: ', headerColumns, index),
onMouseLeave: () => console.log('mouse leave: ', headerColumns, index),
};
};
export default function TableDemo() {
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-solid';
import { Table, Avatar, type ColumnProps, type OnRow } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <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)',
},
};
}
return {};
};
export default function TableDemo() {
return <Table columns={columns} dataSource={data} onRow={handleRow} pagination={false} />;
}实现表头样式定制
可以通过 Column.onHeaderCell 返回特定 style 或 class,定制表头的样式
如下例子,通过传入 background-color 改变了表头背景色
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 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-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const onHeaderCell = () => ({
style: {
'background-color': 'var(--semi-color-fill-0)',
},
});
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 280,
onHeaderCell,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
width: 100,
dataIndex: 'size',
onHeaderCell,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
onHeaderCell,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
onHeaderCell,
},
{
title: '',
dataIndex: 'operate',
onHeaderCell,
render: () => <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',
},
];
export default function TableDemo() {
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-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
width: 100,
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <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',
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={data} class="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-solid';
import { Table, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const filters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
filters,
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: () => <IconMore />,
},
];
const data: Row[] = [
{
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',
},
];
export default function TableDemo() {
return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}设置 ellipsis.showTitle 为 false 可以隐藏默认原生的 HTML title。配合 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-solid';
import { Table, Typography, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const filters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
filters,
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: () => <IconMore />,
},
];
const data: Row[] = [
{
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',
},
];
export default function TableDemo() {
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 | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
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 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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
resize: false,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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: () => <IconMore />,
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} resizable bordered />;
}进阶的伸缩列
resizable 还能为一个 Object,包括三个事件方法:
onResizeonResizeStartonResizeStop
分别触发于列宽改变中、开始改变和结束改变三个时机。开发者可以选择在这个时机修改 column,例如在拉拽时增加一个拖动时的竖线效果等,如下例。
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-11 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 |
代码
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-solid';
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';
}
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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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 resizable: TableResizableProps<Row> = {
onResizeStart: (curColumn) => {
return { class: addClass(curColumn.class, 'my-resizing') };
},
onResizeStop: (curColumn) => {
return { class: removeClass(curColumn.class, 'my-resizing') };
},
};
export default function TableDemo() {
return (
<div id="components-table-demo-resizable-column">
<Table columns={columns} dataSource={dataSource} 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 的 @dnd-kit/solid 搭配 components 属性可轻松实现拖拽排序:components.body.row 传入一个 Solid 组件,它会收到每个数据行的属性(包括 data-row-key)以及 onRow 返回的 index,在其中调用 useSortable 即可。外层用 DragDropProvider 包裹 Table,拖拽结束后在 onDragEnd 中按 source.initialIndex 与 source.index 调整 dataSource。
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-06 | |
| 10 KB | 姜姜鹏志 | 2024-05-06 | |
| 15 KB | 郝郝宣 | 2024-05-06 | |
| 20 KB | 姜姜鹏志 | 2024-05-06 | |
| 25 KB | 郝郝宣 | 2024-05-06 | |
| 30 KB | 姜姜鹏志 | 2024-05-06 | |
| 35 KB | 郝郝宣 | 2024-05-06 | |
| 40 KB | 姜姜鹏志 | 2024-05-06 | |
| 45 KB | 郝郝宣 | 2024-05-06 |
代码
import { createMemo, createSignal, splitProps, type JSX } from 'solid-js';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { arrayMove } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/solid';
import { isSortable, useSortable } from '@dnd-kit/solid/sortable';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnRowReturnObject, type TableProps } from '@fibrous/semi-ui-solid';
interface DemoRow {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: DemoRow[] = [];
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(2024, 4, 6, 12).valueOf() + randomNumber,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
interface SortableRowProps extends JSX.HTMLAttributes<HTMLTableRowElement> {
index: number;
'data-row-key': string;
}
// index comes from the table's onRow; it must not reach the tr
function SortableRow(rawProps: SortableRowProps) {
const [local, rest] = splitProps(rawProps, ['index']);
const { ref, isDragging } = useSortable({
get id() {
return rawProps['data-row-key'];
},
get index() {
return local.index;
},
});
return <tr {...rest} classList={{ isDragging: isDragging() }} ref={(node) => ref(node)} style={{ cursor: 'grabbing', ...(isDragging() ? { 'z-index': 999, position: 'relative' } : {}) }} />;
}
export default function TableDemo() {
const pageSize = 10;
const [dataSource, setData] = createSignal(getData());
const [pageNum, setPageNum] = createSignal(1);
const pageData = createMemo(() => dataSource().slice((pageNum() - 1) * pageSize, pageNum() * pageSize));
const columns: ColumnProps<DemoRow>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src="/assets/placeholder/app.svg" alt="" style={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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: Parameters<typeof DragDropProvider>[0]['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(groupKey) 来定义分组表头的渲染。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
标题 | 所有者 |
|---|
| 100 KB | 姜姜鹏志 | 2024-08-14 | |
| 100 KB | 郝郝宣 | 2024-08-14 | |
| 100 KB | 姜姜鹏志 | 2024-08-14 | |
| 112 KB | 郝郝宣 | 2024-08-26 | |
| 112 KB | 姜姜鹏志 | 2024-08-26 | |
| 112 KB | 郝郝宣 | 2024-08-26 | |
| 105 KB | 姜姜鹏志 | 2024-08-19 | |
| 105 KB | 郝郝宣 | 2024-08-19 | |
| 105 KB | 姜姜鹏志 | 2024-08-19 | |
| 117 KB | 郝郝宣 | 2024-08-31 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowKey = (record: Row) => `${record.owner.toLowerCase()}-${record.name.toLowerCase()}`;
const onGroupedRow = (group: { groupKey: string | number; records: Row[] }, index: number) => {
return {
onClick: () => {
console.log(`Grouped row clicked: `, group, index);
},
};
};
export default function TableDemo() {
return (
<div style={{ padding: '20px 0px' }}>
<Table
dataSource={dataSource}
rowKey={rowKey}
groupBy="size"
columns={columns}
renderGroupSection={(groupKey) => <strong>根据文件大小分组 {groupKey} KB</strong>}
onGroupedRow={onGroupedRow}
clickGroupedRowToExpand // 点击整行即可展开
scroll={{ y: 480 }}
/>
</div>
);
}虚拟化表格
虚拟化可用于需要渲染大规模数据的场景,通过配置 virtualized 参数来开启这个功能。需要注意的是:
- 需要传递每行的高度
virtualized.itemSize(不传时按size取默认行高:default为53,middle为45,small为37,组头行高与普通行相同),可以为如下类型:number(index, { sectionRow?: boolean, expandedRow?: boolean }) => number
- 表格分组虚拟化已支持
virtualized对象另外支持overscanCount:可视区上下额外渲染的行数,默认 2
- 必须传递
scroll.y(number);表格宽度取style.width,未设置时取scroll.x。style落在根元素上,width写成带单位的字符串,如'750px';
通过 getVirtualizedListRef 获取虚拟列表的实例,实例提供 scrollTo(offset) 与 scrollToItem(index, align),align 为 auto(默认)、smart、center、start、end。
以下为渲染 1000 条数据的示例。
标题 | 所有者 |
|---|
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, Button, type ColumnProps, type TableProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
type VirtualizedList = Parameters<NonNullable<TableProps['getVirtualizedListRef']>>[0];
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={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const scroll = { y: 400, x: 900 };
const style = { width: '750px', margin: '0 auto' };
export default function TableDemo() {
let virtualizedList: VirtualizedList | undefined;
return (
<>
<Button onClick={() => virtualizedList?.scrollToItem(100)}>Scroll to 100</Button>
<Table
pagination={false}
columns={columns}
dataSource={dataSource}
scroll={scroll}
style={style}
virtualized
getVirtualizedListRef={(list) => {
virtualizedList = list;
}}
/>
</>
);
}无限滚动
基于虚拟化特性,通过传入 virtualized.onScroll 我们可以实现无限滚动加载数据。
标题 | 所有者 |
|---|
代码
import { createSignal } from 'solid-js';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type TableVirtualizedProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return page;
};
const scroll = { y: 600, x: 1000 };
const style = { width: '750px', margin: '0 auto' };
const itemSize = 56;
export default function TableDemo() {
const [data, setData] = createSignal<Row[]>(getPage(0));
const loadMore = () => {
setData([...data(), ...getPage(data().length)]);
};
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 { createSignal, children as resolveChildren, type JSX } from 'solid-js';
import * as dateFns from 'date-fns';
import { Table, Switch, ButtonGroup, Button, Avatar, type ColumnProps, type RowSelectionProps, type TableProps, type TablePaginationPosition as PaginationPosition } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
description?: string;
}
interface PageState {
currentPage: number;
pageSize: number;
total: number;
}
interface FetchResult {
dataSource: Row[];
pagination: PageState;
sorter: ColumnProps<Row>;
filters: ColumnProps<Row>[];
}
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const dataTotalSize = 46;
const wrapStyle = {
'margin-bottom': '15px',
display: 'flex',
'justify-content': 'space-around',
'flex-wrap': 'wrap',
} as const;
const initialColumns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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: Row[] = 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(2024, 4, 6, 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: '0px' }}>This is footer.</p>;
const mergeColumns = (column: ColumnProps<Row>, columns: ColumnProps<Row>[], keys: (keyof ColumnProps<Row>)[] = ['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<Row>[], dataSource: Row[]) => {
let result = [...dataSource];
filters.forEach((filter) => {
const filteredValue = filter.filteredValue;
const dataIndex = filter.dataIndex as keyof Row | undefined;
if (Array.isArray(filteredValue) && filteredValue.length && dataIndex) {
result = result.filter((data) => filteredValue.filter((value) => String(data[dataIndex]).indexOf(value) > -1).length);
}
});
return result;
};
const sortData = (sortObj: ColumnProps<Row>, dataSource: Row[]) => {
const { sortOrder } = sortObj;
const dataIndex = (sortObj.dataIndex ?? '') as keyof Row;
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<Row> = {}, filters: ColumnProps<Row>[] = []) => {
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<Row>[]) => columns.filter((column) => !!column.sorter)[0];
const getFilterColumns = (columns: ColumnProps<Row>[]) => columns.filter((column) => Array.isArray(column.filteredValue) && column.filteredValue.length);
interface TableSwitchProps {
text: string;
children?: JSX.Element;
checked?: boolean | number | string;
onChange?: (checked: boolean) => void;
}
function TableSwitch(props: TableSwitchProps) {
// Resolve the children once: reading props.children again would build new nodes
const content = resolveChildren(() => props.children);
return (
<span style={{ display: 'inline-flex', 'align-items': 'center', margin: '5px' }}>
<span>{props.text}</span>
{content() ?? <Switch size="small" aria-label={props.text} checked={props.checked != null ? !!props.checked : undefined} onChange={props.onChange} />}
</span>
);
}
export default function TableDemo() {
const [loading, setLoading] = createSignal(true);
const [columns, setColumns] = createSignal<ColumnProps<Row>[]>(initialColumns);
const [scroll, setScroll] = createSignal<TableProps<Row>['scroll']>({});
const [rowSelection, setRowSelection] = createSignal<RowSelectionProps<Row>>();
const [expandable, setExpandable] = createSignal(false);
const [expandCellFixed, setExpandCellFixed] = createSignal(false);
const [showTitle, setShowTitle] = createSignal(false);
const [showFooter, setShowFooter] = createSignal(false);
const [expandedRowKeys, setExpandedRowKeys] = createSignal<string[]>([]);
const [showHeader, setShowHeader] = createSignal(true);
const [bordered, setBordered] = createSignal(false);
const [resizable, setResizable] = createSignal(false);
const [pagination, setPagination] = createSignal<PageState | false>(defaultPagination);
const [dataSource, setDataSource] = createSignal<Row[]>([]);
const load = (currentPagination: PageState, currentPage?: number, sorter?: ColumnProps<Row>, filters?: ColumnProps<Row>[]) => {
fetchData(currentPagination, currentPage, sorter, filters)
.then((result) => {
let nextColumns = mergeColumns(result.sorter, columns());
for (const filterObj of result.filters) {
nextColumns = mergeColumns(filterObj, nextColumns);
}
setColumns(nextColumns);
setLoading(false);
setPagination(result.pagination);
setDataSource(result.dataSource);
})
.catch((err) => {
console.error(err);
setLoading(false);
});
};
const setPage = (page?: number | null, sorter?: ColumnProps<Row> | null, filters?: ColumnProps<Row>[]) => {
if (loading()) {
return;
}
const current = pagination();
const currentPage = typeof page === 'number' ? page : (current && current.currentPage) || 1;
setLoading(true);
load(current || defaultPagination, currentPage, sorter || getSorterColumn(columns()), filters || getFilterColumns(columns()));
};
// Initial load: the first page with the default sorter
load(defaultPagination, 1, getSorterColumn(initialColumns), []);
const toggleFixHeader = (checked: boolean) => {
setScroll({ ...scroll(), y: checked ? 300 : undefined });
};
const toggleFixColumns = (checked: boolean) => {
const cols = columns();
const lastIndex = cols.length - 1;
if (checked) {
setColumns(
cols.map((column, index) => {
if (index === 0) {
return { ...column, fixed: true };
}
return index === lastIndex ? { ...column, fixed: 'right' } : column;
})
);
} else {
setColumns(cols.map((column) => ({ ...column, fixed: false })));
}
const selection = rowSelection();
if (selection) {
setRowSelection({ ...selection, 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 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(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()?.y} onChange={toggleFixHeader} />
<TableSwitch text="隐藏表头:" onChange={(checked) => setShowHeader(!checked)} />
<TableSwitch text="显示标题:" onChange={(checked) => setShowTitle(checked)} />
<TableSwitch text="显示底部:" onChange={(checked) => setShowFooter(checked)} />
<TableSwitch text="固定列:" onChange={toggleFixColumns} />
<TableSwitch text="显示选择列:" onChange={toggleRowSelection} />
<TableSwitch text="显示加载状态:" onChange={(checked) => setLoading(checked)} 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={(checked) => setExpandable(checked)} checked={expandable()} />
<TableSwitch text="展开当前所有行:" onChange={toggleExpandedRowKeys} />
<TableSwitch text="显示边框:" onChange={(checked) => setBordered(checked)} 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.' : undefined}
footer={showFooter() ? renderFooter : undefined}
showHeader={showHeader()}
bordered={bordered()}
onChange={(data) => {
console.log('Table changed: ', data);
setPage(data.pagination?.currentPage, data.sorter, data.filters);
}}
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?: () => JSX.Element, // 筛选按钮的渲染函数,列没有筛选时不存在
sorter?: () => JSX.Element, // 排序按钮的渲染函数,列没有排序时不存在
selection?: () => JSX.Element, // 选择按钮的渲染函数
}
Column.render 第四个入参为一个 object,结构如下:
{
expandIcon?: () => JSX.Element, // 展开按钮的渲染函数
selection?: () => JSX.Element, // 选择按钮的渲染函数
indentText?: () => JSX.Element, // 缩进的渲染函数
}
这些节点都是渲染函数,每次调用都会新建节点,只在要显示的位置调用(如 {parts.sorter?.()}),没有调用的节点不会创建。在 useFullRender 下,title 函数只调用一次,其入参的字段是实时的:在返回的 JSX 中读取字段即可跟随变化。
下方的例子则是将复选框与内容渲染至同一单元格和表头中。
| Name | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-11 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
| 25 KB | 郝郝宣 | 2024-05-31 | |
| 30 KB | 姜姜鹏志 | 2024-06-05 | |
| 35 KB | 郝郝宣 | 2024-06-10 | |
| 40 KB | 姜姜鹏志 | 2024-06-15 | |
| 45 KB | 郝郝宣 | 2024-06-20 | |
| 50 KB | 姜姜鹏志 | 2024-06-25 | |
| 55 KB | 郝郝宣 | 2024-06-30 |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
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>[] = [
{
// Each part is a render function: call it where the button is inserted
title: (props) => (
<span style={{ display: 'inline-flex', 'align-items': 'center', 'padding-left': '20px' }}>
{props.selection?.()}
<span style={{ 'margin-left': '8px' }}>Name</span>
{props.sorter?.()}
{props.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, options) => {
return (
<span style={{ display: 'inline-flex', 'align-items': 'center', 'justify-content': 'center' }}>
{options.indentText?.()}
{options.expandIcon?.()}
{options.selection?.()}
<span style={{ 'margin-left': '8px' }}>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
const pagination = {
pageSize: 12,
};
const rowSelection: RowSelectionProps<Row> = {
hidden: true,
fixed: 'left',
};
export default function TableDemo() {
return <Table pagination={pagination} rowSelection={rowSelection} columns={columns} dataSource={dataSource} onChange={(changeInfo) => console.log(changeInfo)} expandedRowRender={(record) => <article>{record.name}</article>} />;
}表头合并
用户可以通过表头合并功能进行表头的分组,表头合并支持与固定列、虚拟化、数据分组、列伸缩等功能复合使用,也同时支持 JSX 或者配置式写法。
合并表头配置式写法
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| 0 KB | 姜姜鹏志 | 2024-05-06 | |||
| 5 KB | 郝郝宣 | 2024-05-11 | |||
| 10 KB | 姜姜鹏志 | 2024-05-16 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |||
| 20 KB | 姜姜鹏志 | 2024-05-26 | |||
| 25 KB | 郝郝宣 | 2024-05-31 | |||
| 30 KB | 姜姜鹏志 | 2024-06-05 | |||
| 35 KB | 郝郝宣 | 2024-06-10 | |||
| 40 KB | 姜姜鹏志 | 2024-06-15 | |||
| 45 KB | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
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={{ 'margin-right': '12px' }} />
{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={{ 'margin-right': '4px' }}>
{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: () => <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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
export default function TableDemo() {
return <Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={dataSource} scroll={{ x: 1000, y: 400 }} onChange={(changeInfo) => console.log(changeInfo)} columns={columns} />;
}合并表头 JSX 写法
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| 0 KB | 姜姜鹏志 | 2024-05-06 | |||
| 5 KB | 郝郝宣 | 2024-05-11 | |||
| 10 KB | 姜姜鹏志 | 2024-05-16 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |||
| 20 KB | 姜姜鹏志 | 2024-05-26 | |||
| 25 KB | 郝郝宣 | 2024-05-31 | |||
| 30 KB | 姜姜鹏志 | 2024-06-05 | |||
| 35 KB | 郝郝宣 | 2024-06-10 | |||
| 40 KB | 姜姜鹏志 | 2024-06-15 | |||
| 45 KB | 郝郝宣 | 2024-06-20 |
代码
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar } from '@fibrous/semi-ui-solid';
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 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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
const nameFilters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const renderName = (text: string) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
};
const renderOwner = (text: string, record: Row) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
export default function TableDemo() {
return (
<Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={dataSource} scroll={{ x: 1000, y: 400 }} onChange={(changeInfo) => console.log(changeInfo)}>
<Table.Column title="基本信息" fixed="left">
<Table.Column title="标题" dataIndex="name" width={300} fixed render={renderName} filters={nameFilters} onFilter={(value, record) => (record as Row).name.includes(value)} />
<Table.Column title="大小" dataIndex="size" width={100} fixed render={(text) => `${text} KB`} sorter={(a, b) => ((a as Row).size - (b as Row).size > 0 ? 1 : -1)} />
</Table.Column>
<Table.Column title="其他信息">
<Table.Column title="所有者" dataIndex="owner" render={renderOwner} />
<Table.Column title="更新日期" dataIndex="updateTime" sorter={(a, b) => ((a as Row).updateTime - (b as Row).updateTime > 0 ? 1 : -1)} render={(value) => dateFns.format(new Date(value), 'yyyy-MM-dd')} />
</Table.Column>
<Table.Column title="更多" dataIndex="operate" fixed="right" width={100} align="center" render={() => <IconMore />} />
</Table>
);
}行列合并
- 表头除了通过
children写法进行合并外,可通过设置column.colSpan进行表头的列合并。 - 表格支持行/列合并,由
column.onCell返回单元格属性colSpan或者rowSpan,设值为 0 时,该单元格不会渲染,也不调用render。
type OnCell = (record: RecordType, rowIndex: number) => JSX.TdHTMLAttributes<HTMLElement>;
| 标题 | 大小 | 所有者 | |
|---|---|---|---|
| 5 KB | 郝郝宣 | 2024-05-11 | |
姜姜鹏志 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
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,
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 4 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
render: (text) => (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
),
},
{
title: '大小',
dataIndex: 'size',
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 0 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
onCell: (_record, index) => (index === 0 ? { colSpan: 0 } : {}),
render: (text, record) => (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
),
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 0 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
render: (value) => dateFns.format(new Date(value), 'yyyy-MM-dd'),
},
];
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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(5);
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} pagination={false} />;
}API 参考
Table
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| bordered | 是否展示外边框和列边框。 | boolean | false |
| children | Table.Column 子项,未传入 columns 时读取它们。 | JSX.Element | - |
| childrenRecordName | 树形表格中,数据项里表示子级数据的字段名。 | string | "children" |
| clickGroupedRowToExpand | 点击分组行时展开或收起该组。 | boolean | - |
| columns | 列配置,详见 Column。传入时(包括空数组)优先于 Table.Column 子项。 | TypeColumnProps<RecordType>[] | - |
| components | 覆盖 Table 的组成元素,如 table、body、row、td、th,值为标签名或组件;组件会收到被替换元素的属性与事件。 | TableComponents | - |
| dataSource | 数据。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。 | RecordType[] | [] |
| defaultExpandAllGroupRows | 默认是否展开所有分组行,动态加载数据时不生效。 | boolean | false |
| defaultExpandAllRows | 默认是否展开所有行,动态加载数据时不生效。 | boolean | false |
| defaultExpandedRowKeys | 默认展开的行 key 数组,动态加载数据时不生效。 | (string | number)[] | [] |
| direction | 文字方向,默认值取 ConfigProvider 的 direction,可在此单独配置。 | Direction | - |
| empty | 无数据时展示的内容,默认为语言包中的空状态文案。 | JSX.Element | - |
| 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 | 虚拟化表格创建列表时调用,参数提供 scrollTo 与 scrollToItem,仅在配置 virtualized 时有效。 | GetVirtualizedListRef | - |
| groupBy | 分组依据,可以是数据项中的字段名,或返回字符串、数字的函数。 | TypeGroupBy<RecordType> | - |
| headerStyle | 所有表头单元格(包括固定表头)的内联样式,属性名使用 kebab-case。 | JSX.CSSProperties | - |
| hideExpandedColumn | 表格可展开时,展开按钮默认与第一列内容渲染在同一个单元格;设为 false 时展开按钮单独占一列。 | boolean | true |
| indentSize | 树形数据每一层的缩进宽度,单位为像素。 | number | 20 |
| keepDOM | 折叠行时是否保留被折叠的 DOM。 | boolean | - |
| loading | 是否显示加载状态。 | boolean | false |
| onChange | 分页、排序、筛选变化时触发,参数 changeInfo 包含 pagination、filters、sorter 与 extra。 | TypeOnChange<RecordType> | - |
| onExpand | 点击行展开图标时触发,参数为是否展开、该行数据与鼠标事件。 | TypeOnExpand<RecordType> | - |
| onExpandedRowsChange | 展开的行变化时触发,参数为展开行的数据数组。 | TypeOnExpandedRowsChange<RecordType> | - |
| onGroupedRow | 类似于 onRow,单独用于分组行的属性与事件,返回的对象会合并到分组行的 tr 上。index 在未开启 virtualized 时是分组序号,开启后是该行在虚拟列表中的位置。 | TypeOnGroupedRow<RecordType> | - |
| onHeaderRow | 设置表头行的属性与事件,返回的对象会合并到表头行的 tr 上(不含 style,请用 headerStyle)。 | TypeOnHeaderRow<RecordType> | - |
| onRow | 设置数据行的属性与事件,第三个参数为行状态。返回的对象会合并到数据行的 tr 上,其中的事件在行自身的处理和 onRowXxx 事件之后调用。 | TypeOnRow<RecordType> | - |
| onRowContextMenu | 数据行触发右键菜单时的回调,先于 onRow 返回的 onContextMenu 调用,两者都会执行。 | Type(record: RecordType, e: MouseEvent) => void | - |
| onRowDoubleClick | 双击数据行时的回调,先于 onRow 返回的 onDblClick 调用,两者都会执行。 | Type(record: RecordType, e: MouseEvent) => void | - |
| onRowMouseEnter | 鼠标进入数据行时的回调,先于 onRow 返回的 onMouseEnter 调用,两者都会执行。 | Type(record: RecordType, e: MouseEvent) => void | - |
| onRowMouseLeave | 鼠标离开数据行时的回调,先于 onRow 返回的 onMouseLeave 调用,两者都会执行。 | Type(record: RecordType, e: MouseEvent) => void | - |
| pagination | 分页配置,传 false 关闭分页。 | TablePagination | true |
| prefixCls | 样式名前缀。 | string | "semi-table" |
| ref | 根元素,带有 getCurrentPageData 方法,见下方方法。 | TypeRef<TableElement<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> | - |
| rowSpanHover | 悬停在带 rowSpan 的单元格上时,高亮它跨越的所有行。 | boolean | - |
| scroll | 滚动区域配置,x 为横向宽度,y 为纵向高度,详见 scroll。 | Scroll | - |
| showHeader | 是否显示表头。 | boolean | true |
| size | 表格尺寸,影响单元格的内边距。 | Size | "default" |
| sticky | 将表头固定在页面顶部。传对象时 top 为距离滚动容器顶部的距离。 | Sticky | - |
| title | 表格标题,可以是元素,或接收当前页数据返回元素的函数。 | TypeTitle<RecordType> | - |
| virtualized | 开启虚拟化,需同时传 scroll.y(数字)。对象形式可配置行高 itemSize、滚动回调 onScroll 与额外渲染的行数 overscanCount。 | Virtualized | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| getCurrentPageData | 返回当前页的数据(深拷贝),形如 { dataSource, groups }。 | Type() => Pick<BasePageData<RecordType>, 'dataSource' | 'groups'> |
class、style、id、data-* 等原生属性落在最外层元素;aria-label、aria-labelledby、aria-describedby 落在 role="table" 的元素上。通过 ref 获取的实例提供 getCurrentPageData 等方法,见下文「方法」。
headerStyle 示例
headerStyle 会应用到所有表头 <th> 元素,包括 fixed 表头。
onHeaderRow / onRow 用法
onHeaderRow 中可以返回 th 支持的属性或者事件 onRow 中可以返回 tr 支持的属性或者事件
onRow 的第三个参数 rowStatus 可以获取当前行的状态信息,包括 disabled 和 selected 属性。这在需要根据行的选中或禁用状态执行不同逻辑时非常有用,例如点击行时判断是否允许选中。
Column
列配置只接受下表的属性,拼错的属性名会报类型错误。需要在列上携带自定义字段时,用交叉类型扩展,如 ColumnProps<RecordType> & { tooltip: string }。
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | 列内容的对齐方式。 | Align | - |
| children | 嵌套的 Table.Column 子项,用于表头分组。 | JSX.Element | - |
| class | 列的类名,加在该列的 col 元素上。 | string | - |
| colSpan | 表头单元格的 colSpan。 | number | - |
| dataIndex | 列数据在数据项中对应的字段,使用 sorter 或 onFilter 时必须同时提供唯一的 dataIndex。 | string | - |
| defaultFilteredValue | 默认的筛选值,非受控时作为初始筛选状态。 | any[] | - |
| defaultSortOrder | 默认的排序方向,非受控时作为初始排序状态。 | SortOrder | - |
| ellipsis | 文本超出单元格时省略显示;传对象时可配置 showTitle(原生 title 提示)与 showTooltip(Tooltip 提示)。 | BaseEllipsis | - |
| filterChildrenRecord | 树形表格中筛选时是否同时筛选子数据。 | boolean | - |
| filterConfirmMode | 筛选确认模式:immediate 选中即筛选,confirm 点击确认按钮后再筛选。 | FilterConfirmMode | - |
| filterDropdown | 渲染替换筛选触发器与菜单的内容。 | Type() => JSX.Element | - |
| filterDropdownProps | 筛选下拉菜单的属性,与 Dropdown 相同,但不含 render 与 onVisibleChange。 | FilterDropdownProps | - |
| filterDropdownVisible | 筛选下拉菜单是否可见。 | boolean | - |
| filteredValue | 筛选的受控值,传入后筛选状态由使用方控制。 | any[] | - |
| filterIcon | 筛选图标,接收 filtered 返回元素的函数,筛选状态变化时重新调用。 | FilterIcon | - |
| filterMultiple | 筛选菜单是否可以多选。 | boolean | - |
| filters | 表头的筛选菜单配置,使用 renderFilterDropdown 时可以不传。选项的 text 是文本,带标记的选项用 renderFilterDropdownItem。 | Filter[] | - |
| fixed | 列固定的位置,left 或 right,true 等同于 left。 | Fixed | - |
| key | 列的标识,未传入时由 dataIndex 或列的位置生成。 | string | number | - |
| onCell | 设置数据单元格的属性与事件,参数为数据项与行索引,返回的对象会合并到 td 上;colSpan 或 rowSpan 为 0 时不渲染该单元格,也不调用 render。 | TypeOnCell<RecordType> | - |
| onFilter | 筛选函数,接收筛选值与数据项,返回 true 时保留该数据项。 | TypeOnFilter<RecordType> | - |
| onFilterDropdownVisibleChange | 筛选下拉菜单显示状态变化时的回调。 | OnFilterDropdownVisibleChange | - |
| onHeaderCell | 设置表头单元格的属性与事件,参数为列、列索引与行索引,返回的对象会合并到 th 上。 | TypeOnHeaderCell<RecordType> | - |
| render | 自定义单元格内容,参数为 (text, record, index, options),返回元素;单元格属性与合并用 onCell。options 的字段是实时的,在返回的 JSX 中读取;expandIcon、selection、indentText 是渲染函数,在要显示的位置调用。 | TypeColumnRender<RecordType> | - |
| renderFilterDropdown | 自定义筛选下拉菜单的内容,接收的参数包含临时筛选值、设置方法与确认、清除、关闭方法,参数的字段是实时的,在返回的 JSX 中读取。 | Type(props: RenderFilterDropdownProps) => JSX.Element | - |
| renderFilterDropdownItem | 自定义筛选菜单的每一项,接收该项的文本、选中状态与 onChange。 | RenderFilterDropdownItem | - |
| resize | 列开启 resizable 时,传 false 让该列没有伸缩手柄。 | boolean | - |
| showSortTip | 排序时是否显示排序提示。 | boolean | - |
| sortChildrenRecord | 树形表格中排序时是否同时排序子数据。 | boolean | - |
| sorter | 排序函数,接收两个数据项(以及当前排序方向),返回负数、0 或正数。启用排序时必须同时提供 dataIndex。 | TypeSorter<RecordType> | - |
| sortIcon | 排序图标,接收 sortOrder 并返回元素。sortOrder 是实时的,在返回的 JSX 中读取。 | SortIcon | - |
| sortOrder | 排序的受控方向,传入后排序状态由使用方控制。 | SortOrder | - |
| title | 表头内容。可以是文本,或接收 sorter、filter、selection 渲染函数并返回元素的函数,在要显示的位置调用它们;使用 useFullRender 时这些字段是实时的。 | ColumnTitle | - |
| useFullRender | 是否由列接管表头与单元格的完整渲染,开启后可以在 title 与 render 中调用 sorter、filter、selection、expandIcon 等节点的渲染函数。 | boolean | - |
| width | 列宽,数字为像素。使用 resizable 时需要提供。 | string | number | - |
Column 是 Table 的子组件声明,本身不渲染任何内容,Table 读取它的 props 作为列配置。配置写法与 Table.Column 写法的列属性相同。
Column.onCell / onHeaderCell 用法
与 onRow、onHeaderRow类似,在 column.onCell column.onHeaderCell 中也能返回 td/th 支持的属性或事件
rowSelection
scroll
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| scrollToFirstRowOnChange | 当分页、排序、筛选变化后是否自动滚动到表格顶部。当设置了 scroll.y 时,会重置表格内部滚动位置到顶部;当未设置 scroll.y 时,会滚动页面到表格头部位置 | boolean | false |
| x | 设置横向滚动区域的宽,可以为像素值、百分比或 'max-content' | string|number | |
| y | 设置纵向滚动区域的高,可以为像素值 | number |
pagination
翻页组件配置。pagination 建议不要使用字面量写法。
注意:pagination.onChange 设置后,Table onChange 不再响应分页器变化。
其他配置详见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 提供的一些内部方法:
| 名称 | 描述 |
|---|---|
| getCurrentPageData() | 返回当前页的数据对象:{ dataSource: RecordType[], groups: Map<string | number, Set<string | number>> },groups 的键是分组键,值是该组的行键集合 |
Accessibility
ARIA
Table 按 WAI-ARIA Table 输出静态表格语义,单元格之间不用方向键移动,可交互内容(排序、筛选、展开、选择)都是可以 Tab 聚焦的原生控件。
- 表格的 role 为 table,行为 row,表头单元格为 columnheader,单元格为 cell;树形表格同样是 table,不输出行的层级
- 排序按钮是表头里的原生
button,以列标题命名;当前排序方向写在表头单元格的aria-sort上(ascending或descending),未排序的列不带该属性。showSortTip的提示在按钮聚焦时显示 - 展开按钮是原生
button,名称为「展开此行」,aria-expanded表示该行是否展开 - 行选择的复选框带有「选择此行」「全选所有行」等可访问名称
- 固定表头(
scroll.y、sticky)或虚拟化时,表头和表体渲染为两个 table 元素,二者的共同容器是 role 为 table 的元素,内部 table 为 role none,单元格仍与列头关联 - 虚拟化表格的
aria-rowcount为表头行数加数据总行数,数据行的aria-rowindex接在表头行之后,屏幕阅读器据此报出不在 DOM 中的行
已知限制:
- 带排序的列,
title渲染在排序按钮里,title中不要放按钮、链接等可交互内容
- 默认的列筛选图标是原生
button,可用 Tab 聚焦,Enter、Space 或下箭头打开筛选菜单,并带有aria-haspopup="menu"与aria-expanded;自定义filterIcon时由使用方保证其可聚焦
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 更新后,会将页码重置到初始态。请检查数据源是否在组件渲染时发生了变化。
组件只执行一次,
dataSource应放在 signal 中,并在 JSX 里读取(如dataSource={data()})。替换整个数组会触发更新;原地修改数组(如push)不会触发,需要改为生成新数组后setData。 -
筛选后的数据条数不对?
请检查你的筛选列和数据源是否配置正确。
筛选列需设置独立的 dataIndex,同时 dataSource 需要设置独立的 key,请参考 dataSource API。否则筛选功能无法正常工作。
-
表格数据为何没有更新?
Table 按 props 的读取位置更新,
columns、dataSource等在 JSX 中读取 signal 即可跟随变化。columns数组定义在组件内部时只会创建一次,依赖 signal 的列配置请使用 getter 或在render中读取。 -
为何我的表格行不能选中以及展开?
请指定 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 暂不支持单行选择功能,用户可以通过自定义方式实现单选。