Show · Table

Table

Tables are used to present structured data content, usually accompanied by the ability to manipulate the data (sort, search, paginate...).

How to Use

Into the header. columns And data. DataSource To render.

Please provide a "key" for each data item in the dataSource that is different from the value of the other data items, or use the row Key parameter to specify an attribute name as the primary key, alternative row operation functions such as row selection and expansion of the table.

import React from 'react';
import { Table } from '@fibrous/semi-ui-react';

function App() {
    const columns = [
        {
            title: 'Title',
            dataIndex: 'name',
        },
        {
            title: 'Size',
            dataIndex: 'size',
        },
        {
            title: 'Owner',
            dataIndex: 'owner',
        },
        {
            title: 'Update',
            dataIndex: 'updateTime',
        },
    ];
    const data = [
        {
            key: '1',
            name: 'Semi Design design draft.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: 'Jiang Pengzhi',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design share docs',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: 'Hao Xuan',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: 'Design docs',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    return <Table columns={columns} dataSource={data} pagination={false} />;
}

Demos

Basic Table

For tables, the two most basic parameters are dataSource and columns, the former is the data item, the latter is the configuration of each column, both are array types.

标题大小交付状态所有者更新日期
Semi Design 设计稿.fig
2M
已交付
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿
2M
已延期
郝郝宣
2020-01-17 05:31
设计文档
34KB
待评审
ZZoey Edwards
2020-01-26 11:01
Code
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-react';

export default function TableDemo() {
    const columns: ColumnProps[] = [
        {
            title: '标题',
            dataIndex: 'name',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
        },
        {
            title: '大小',
            dataIndex: 'size',
        },
        {
            title: '交付状态',
            dataIndex: 'status',
            render: (text: 'success' | 'pending' | 'wait') => {
                const tagConfig = {
                    success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                    pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                    wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
                } as const;
                const tagProps = tagConfig[text];
                return (
                    <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                        {tagProps.text}
                    </Tag>
                );
            },
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
        },
        {
            title: '',
            dataIndex: 'operate',
            render: () => {
                return <IconMore />;
            },
        },
    ];
    const data = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            status: 'success',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            status: 'pending',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            status: 'wait',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    return <Table columns={columns} dataSource={data} pagination={false} />;
}

JSX Writing

You can also use JSX syntax definitions columns, note that Table only supports columns JSX syntax definition. You cannot use any component package Table.Column.

Note: JSX-written tables are not supported the parameter Resizable.

标题大小所有者更新时间
Semi Design 设计稿.fig
2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿
2M
郝郝宣
2020-01-17 05:31
设计文档
34KB
ZZoey Edwards
2020-01-26 11:01
Code
import type { ComponentProps } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar } from '@fibrous/semi-ui-react';

const { Column } = Table;

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    status: string;
    updateTime: string;
    avatarBg: ComponentProps<typeof Avatar>['color'];
}

export default function TableDemo() {
    const data: Row[] = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            status: 'success',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            status: 'pending',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            status: 'wait',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    const renderName = (text: string, record: Row) => {
        return (
            <div>
                <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                {text}
            </div>
        );
    };

    const renderOwner = (text: string, record: Row) => {
        return (
            <div>
                <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                    {typeof text === 'string' && text.slice(0, 1)}
                </Avatar>
                {text}
            </div>
        );
    };

    return (
        <Table dataSource={data} pagination={false}>
            <Column title="标题" dataIndex="name" key="name" render={renderName} />
            <Column title="大小" dataIndex="size" key="size" />
            <Column title="所有者" dataIndex="owner" key="owner" render={renderOwner} />
            <Column title="更新时间" dataIndex="updateTime" key="updateTime" />
            <Column title="" dataIndex="operate" key="operate" render={() => <IconMore />} />
        </Table>
    );
}

Row Selection Operation

This feature can be turned on by passing in rowSelection.

  • Click the selection box in the header, and all rows in the dataSource that are not in the state of disabled will be selected. The callback function for selecting all rows is onSelectAll;
  • Clicking on the row selection box will select the current row. Its callback function is onSelect;
标题大小交付状态所有者更新日期
Semi Design 设计稿.fig
2M
已交付
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿
2M
已延期
郝郝宣
2020-01-17 05:31
设计文档
34KB
待评审
ZZoey Edwards
2020-01-26 11:01
显示第 1 条-第 3 条,共 6 条
Code
import { useMemo } from 'react';
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    status: 'success' | 'pending' | 'wait';
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

export default function TableDemo() {
    const columns = useMemo<ColumnProps<Row>[]>(
        () => [
            {
                title: '标题',
                dataIndex: 'name',
                width: 400,
                render: (text, record) => {
                    return (
                        <div>
                            <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                            {text}
                        </div>
                    );
                },
            },
            {
                title: '大小',
                dataIndex: 'size',
            },
            {
                title: '交付状态',
                dataIndex: 'status',
                render: (text: Row['status']) => {
                    const tagConfig = {
                        success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                        pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                        wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
                    } as const;
                    const tagProps = tagConfig[text];
                    return (
                        <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                            {tagProps.text}
                        </Tag>
                    );
                },
            },
            {
                title: '所有者',
                dataIndex: 'owner',
                render: (text, record) => {
                    return (
                        <div>
                            <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                                {typeof text === 'string' && text.slice(0, 1)}
                            </Avatar>
                            {text}
                        </div>
                    );
                },
            },
            {
                title: '更新日期',
                dataIndex: 'updateTime',
            },
            {
                title: '',
                dataIndex: 'operate',
                render: () => {
                    return <IconMore />;
                },
            },
        ],
        []
    );

    const data = useMemo<Row[]>(
        () => [
            {
                key: '1',
                name: 'Semi Design 设计稿.fig',
                nameIconSrc: '/assets/placeholder/app.svg',
                size: '2M',
                owner: '姜鹏志',
                status: 'success',
                updateTime: '2020-02-02 05:13',
                avatarBg: 'grey',
            },
            {
                key: '2',
                name: 'Semi Design 分享演示文稿',
                nameIconSrc: '/assets/docs-icon.png',
                size: '2M',
                owner: '郝宣',
                status: 'pending',
                updateTime: '2020-01-17 05:31',
                avatarBg: 'red',
            },
            {
                key: '3',
                name: '设计文档',
                nameIconSrc: '/assets/docs-icon.png',
                size: '34KB',
                status: 'wait',
                owner: 'Zoey Edwards',
                updateTime: '2020-01-26 11:01',
                avatarBg: 'light-blue',
            },
            {
                key: '4',
                name: 'Semi D2C 设计稿.fig',
                nameIconSrc: '/assets/placeholder/app.svg',
                size: '2M',
                owner: '姜鹏志',
                status: 'wait',
                updateTime: '2020-02-02 05:13',
                avatarBg: 'grey',
            },
            {
                key: '5',
                name: 'Semi D2C 分享演示文稿',
                nameIconSrc: '/assets/docs-icon.png',
                size: '2M',
                owner: '郝宣',
                status: 'pending',
                updateTime: '2020-01-17 05:31',
                avatarBg: 'red',
            },
            {
                key: '6',
                name: 'Semi D2C 设计文档',
                nameIconSrc: '/assets/docs-icon.png',
                size: '34KB',
                status: 'success',
                owner: 'Zoey Edwards',
                updateTime: '2020-01-26 11:01',
                avatarBg: 'light-blue',
            },
        ],
        []
    );

    const rowSelection: RowSelectionProps<Row> = {
        getCheckboxProps: (record) => ({
            disabled: record.name === '设计文档', // 这一行不可勾选
            name: record.name,
        }),
        onSelect: (record, selected) => {
            console.log(`select row: ${selected}`, record);
        },
        onSelectAll: (selected, selectedRows) => {
            console.log(`select all rows: ${selected}`, selectedRows);
        },
        onChange: (selectedRowKeys, selectedRows) => {
            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
        },
    };

    const pagination = useMemo(
        () => ({
            pageSize: 3,
        }),
        []
    );

    return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={pagination} />;
}

Custom Rendering

Users can use Column.render to customize the rendering of a column of cells, which is suitable for rendering more complex cell content.

The fourth parameter options of the render function is an object containing the following properties:

  • expandIcon: Expand icon (when using tree data or expandable rows)
  • selection: Selection checkbox (when row selection is enabled)
  • indentText: Indent content (when using tree data)
  • isHovering: Whether the current row is in hover state

With the isHovering parameter, you can implement interaction effects such as displaying action buttons on mouse hover.

标题大小交付状态所有者更新日期
Semi Design 设计稿标题可能有点长这时候应该显示 Tooltip.fig2M
已交付
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿2M
已延期
郝郝宣
2020-01-17 05:31
设计文档34KB
待评审
ZZoey Edwards
2020-01-26 11:01
Semi D2C 设计文档可能也有点长所以也会显示 Tooltip34KB
已交付
姜姜琪
2020-01-26 11:01
Code
import { useState } from 'react';
import { IconTickCircle, IconComment, IconClear, IconDelete } from '@fibrous/semi-icons-react';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrous/semi-illustrations-react';
import { Table, Avatar, Button, Empty, Typography, Tag, type ColumnProps } from '@fibrous/semi-ui-react';
const { Text } = Typography;

const raw = [
    {
        key: '1',
        name: 'Semi Design 设计稿标题可能有点长这时候应该显示 Tooltip.fig',
        nameIconSrc: '/assets/placeholder/app.svg',
        size: '2M',
        owner: '姜鹏志',
        status: 'success',
        updateTime: '2020-02-02 05:13',
        avatarBg: 'grey',
    },
    {
        key: '2',
        name: 'Semi Design 分享演示文稿',
        nameIconSrc: '/assets/docs-icon.png',
        size: '2M',
        owner: '郝宣',
        status: 'pending',
        updateTime: '2020-01-17 05:31',
        avatarBg: 'red',
    },
    {
        key: '3',
        name: '设计文档',
        nameIconSrc: '/assets/docs-icon.png',
        size: '34KB',
        status: 'wait',
        owner: 'Zoey Edwards',
        updateTime: '2020-01-26 11:01',
        avatarBg: 'light-blue',
    },
    {
        key: '4',
        name: 'Semi D2C 设计文档可能也有点长所以也会显示 Tooltip',
        nameIconSrc: '/assets/docs-icon.png',
        size: '34KB',
        status: 'success',
        owner: '姜琪',
        updateTime: '2020-01-26 11:01',
        avatarBg: 'green',
    },
];

export default function TableDemo() {
    const [dataSource, setData] = useState(raw);

    const removeRecord = (key: string) => {
        let newDataSource = [...dataSource];
        let idx = newDataSource.findIndex((data) => data.key === key);

        if (idx > -1) {
            newDataSource.splice(idx, 1);
            setData(newDataSource);
        }
    };
    const resetData = () => {
        const newDataSource = [...raw];
        setData(newDataSource);
    };

    const columns: ColumnProps[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 400,
            render: (text, record) => {
                return (
                    <span style={{ display: 'flex', alignItems: 'center' }}>
                        <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                        {/* 宽度 = 单元格设定宽度 − 非文本内容宽度 */}
                        <Text ellipsis={{ showTooltip: true }} style={{ width: 'calc(400px - 76px)' }}>
                            {text}
                        </Text>
                    </span>
                );
            },
        },
        {
            title: '大小',
            dataIndex: 'size',
            width: 150,
        },
        {
            title: '交付状态',
            dataIndex: 'status',
            render: (text: 'success' | 'pending' | 'wait') => {
                const tagConfig = {
                    success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                    pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                    wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
                } as const;
                const tagProps = tagConfig[text];
                return (
                    <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                        {tagProps.text}
                    </Tag>
                );
            },
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            width: 300,
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            width: 200,
        },
        {
            title: '',
            dataIndex: 'operate',
            render: (_text, record) => <Button aria-label="删除" icon={<IconDelete />} theme="borderless" onClick={() => removeRecord(record.key)} />,
        },
    ];

    const empty = <Empty image={<IllustrationNoResult />} darkModeImage={<IllustrationNoResultDark />} description="搜索无结果" />;

    return (
        <>
            <Button onClick={resetData} style={{ marginBottom: 10 }}>
                重置
            </Button>
            <Table style={{ minHeight: 350 }} columns={columns} dataSource={dataSource} pagination={false} empty={empty} />
        </>
    );
}

Table With Pagination

Table paging currently supports two modes: controlled and uncontrolled.

  • In controlled mode, the paging state is passed entirely externally, depending on whether the pagination .currentPage field is passed. In general, the controlled mode is suitable for remotely pulling data and rendering.
  • In uncontrolled mode, Table passes the incoming dataSource length as total to the Pagination component by default, and of course you can also pass a total field to overwrite the value of the Table component, but we do not recommend users to pass this field in uncontrolled paging mode.

Note: The custom pagination.total field passed in uncontrolled conditions is supported.

标题
交付状态所有者
Semi Design 设计稿0.fig
0 KB
已交付
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
待评审
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
已交付
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
待评审
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
已交付
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig
25 KB
待评审
郝郝宣
2019-08-17
Semi Design 设计稿6.fig
30 KB
已交付
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig
35 KB
待评审
郝郝宣
2019-08-27
Semi Design 设计稿8.fig
40 KB
已交付
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig
45 KB
待评审
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    status: 'success' | 'wait';
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '交付状态',
        dataIndex: 'status',
        render: (text: 'success' | 'pending' | 'wait') => {
            const tagConfig = {
                success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
            } as const;
            const tagProps = tagConfig[text];
            return (
                <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                    {tagProps.text}
                </Tag>
            );
        },
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const DAY = 24 * 60 * 60 * 1000;

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            status: isSemiDesign ? 'success' : 'wait',
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);

    const rowSelection = useMemo(
        (): RowSelectionProps<Row> => ({
            onChange: (selectedRowKeys, selectedRows) => {
                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
            },
            getCheckboxProps: (record) => ({
                disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
                name: record.name,
            }),
        }),
        []
    );
    const scroll = useMemo(() => ({ y: 300 }), []);

    return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}

Pull Remote Data

Under normal circumstances, the data is often not obtained at one time. We will retrieve the data from the interface when clicking on the page number, filter or sort button. In this case, please use Controlled mode To handle pagination. The user needs to pass in the pagination.currentPage field, where the rendering of the pagination component depends entirely on the incoming pagination object.

标题
交付状态所有者
暂无数据
显示第 1 条-第 5 条,共 46 条
Code
import { useState, useEffect } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pageSize = 5;

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    status: 'success' | 'wait';
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '交付状态',
        dataIndex: 'status',
        render: (text: 'success' | 'pending' | 'wait') => {
            const tagConfig = {
                success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
            } as const;
            const tagProps = tagConfig[text];
            return (
                <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                    {tagProps.text}
                </Tag>
            );
        },
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            status: isSemiDesign ? 'success' : 'wait',
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

const data = getData();

const fetchData = (currentPage: number) => {
    return new Promise<Row[]>((res) => {
        setTimeout(() => {
            res(data.slice((currentPage - 1) * pageSize, currentPage * pageSize));
        }, 300);
    });
};

export default function TableDemo() {
    const [dataSource, setData] = useState<Row[]>([]);
    const [loading, setLoading] = useState(true);
    const [currentPage, setPage] = useState(1);

    const handlePageChange = (page: number) => {
        setLoading(true);
        setPage(page);
    };

    useEffect(() => {
        // Drop responses for pages the user has left
        let ignore = false;
        void fetchData(currentPage).then((dataSource) => {
            if (!ignore) {
                setLoading(false);
                setData(dataSource);
            }
        });
        return () => {
            ignore = true;
        };
    }, [currentPage]);

    return (
        <Table
            columns={columns}
            dataSource={dataSource}
            pagination={{
                currentPage,
                pageSize: 5,
                total: data.length,
                onPageChange: handlePageChange,
            }}
            loading={loading}
        />
    );
}

Fixed Column or Head

You can fix the column by setting the Fixed attribute of the column and scroll.x, and fix the header by setting scroll.y.

  • It is recommended to specify scroll.x as a fixed value or percentage greater than the width of the table. If it is a fixed value, set it to >= the sum of all fixed column widths + the sum of all table column widths.
  • Make sure that all elements inside the table do not affect the height of the cells after rendering (e.g. containing unloaded pictures, etc.). In this case, give the stator element a definite height to ensure that the left and right Fixed columns of cells are not deranged.
  • If the column header is not aligned with the content or there is a column duplication or when the fixed column fails, specify the width width of the fixed column, if still not effective, try to recommend leaving a column with no width to accommodate the elastic layout, or check for ultra-long continuous fields to destroy the layout.
标题
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig
25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig
35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig
45 KB
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        fixed: true,
        width: 250,
        render: (text) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        width: 200,
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        width: 200,
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        width: 200,
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
    {
        title: '',
        dataIndex: 'operate',
        fixed: 'right',
        align: 'center',
        width: 100,
        render: () => {
            return <IconMore />;
        },
    },
];

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);

    const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
    const rowSelection = useMemo<RowSelectionProps<Row>>(
        () => ({
            onChange: (selectedRowKeys, selectedRows) => {
                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
            },
            getCheckboxProps: (record) => ({
                disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
                name: record.name,
            }),
            fixed: true,
        }),
        []
    );

    return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}

The header can be fixed to the top of the page with the sticky property. When passing top, you can control the distance from the scroll container.

After turning on sticky, Table will automatically turn on the fixed layout, and the column width will be determined by column.width. Columns without a given width are automatically assigned by the browser.

import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@fibrous/semi-ui-react';
import { IconMore } from '@fibrous/semi-icons-react';
import * as dateFns from 'date-fns';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

const columns = [
    {
        title: '标题',
        dataIndex: 'name',
        fixed: true,
        width: 250,
        render: (text, record, index) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        width: 200,
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        width: 200,
        render: (text, record, index) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        width: 200,
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
    {
        title: '',
        dataIndex: 'operate',
        fixed: 'right',
        align: 'center',
        width: 100,
        render: () => {
            return <IconMore />;
        },
    },
];

function App() {
    const [dataSource, setData] = useState([]);

    const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
    const rowSelection = useMemo(
        () => ({
            onChange: (selectedRowKeys, selectedRows) => {
                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
            },
            getCheckboxProps: (record) => ({
                disabled: record.name === 'Michael James', // Column configuration not to be checked
                name: record.name,
            }),
            fixed: true,
        }),
        []
    );

    const getData = () => {
        const data = [];
        for (let i = 0; i < 46; i++) {
            const isSemiDesign = i % 2 === 0;
            const randomNumber = (i * 1000) % 199;
            data.push({
                key: '' + i,
                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
                owner: isSemiDesign ? '姜鹏志' : '郝宣',
                size: randomNumber,
                updateTime: new Date().valueOf() + randomNumber * DAY,
                avatarBg: isSemiDesign ? 'grey' : 'red',
            });
        }
        return data;
    };

    useEffect(() => {
        const data = getData();
        setData(data);
    }, []);

    return <Table sticky={{ top: 60 }} columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}

render(App);

Table Header With Sorting and Filtering Function

Filters and sorting controls are integrated inside the table, and users can pass in the sorter display of the sorter open header by passing filters in Column and the filter control display of the onFilter open header.

Note: Be sure to provide a "key" for each row of data that is different from other row values, or use the rowKey parameter to specify a property name as the primary key.

Note: Sorting and filtering columns must set independent "dataIndex"

交付状态所有者
Semi Design 设计稿0.fig
0 KB
已交付
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
待评审
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
已交付
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
待评审
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
已交付
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig
25 KB
待评审
郝郝宣
2019-08-17
Semi Design 设计稿6.fig
30 KB
已交付
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig
35 KB
待评审
郝郝宣
2019-08-27
Semi Design 设计稿8.fig
40 KB
已交付
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig
45 KB
待评审
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    status: 'success' | 'wait';
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
        sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '交付状态',
        dataIndex: 'status',
        render: (text: 'success' | 'pending' | 'wait') => {
            const tagConfig = {
                success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
            } as const;
            const tagProps = tagConfig[text];
            return (
                <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                    {tagProps.text}
                </Tag>
            );
        },
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            status: isSemiDesign ? 'success' : 'wait',
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);

    const rowSelection = useMemo(
        (): RowSelectionProps<Row> => ({
            onChange: (selectedRowKeys, selectedRows) => {
                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
            },
            getCheckboxProps: (record) => ({
                disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
                name: record.name,
            }),
        }),
        []
    );
    const scroll = useMemo(() => ({ y: 300 }), []);

    return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}

When sorter is a function type, the sortOrder status can be obtained through the third parameter of the function. The function type is (a?: RecordType, b?: RecordType, sortOrder?: 'ascend' | 'descend') => number.

You can control whether to display the sorting tip through the showSortTip attribute, which defaults to false. When the tip is turned on, when there is only sorting function, the sorting prompt will be displayed when the mouse is moved to the table header; in other cases, the sorting prompt will be displayed only when the mouse is moved to the sorting icon.

Note: When using the sortOrder attribute for controlled sorting, since the next sort order cannot be predicted, showSortTip does not take effect and the prompt will not be displayed.

标题
所有者更新日期
Semi Design 设计稿.fig
3 KB
姜姜鹏志
2020-02-02
Semi Design 分享演示文稿
未知
郝郝宣
2020-01-17
设计文档 3
1 KB
ZZoey Edwards
2020-01-26
设计文档 4
5 KB
ZZoey Edwards
2020-01-26
设计文档 5
未知
ZZoey Edwards
2020-01-26
设计文档 6
2 KB
ZZoey Edwards
2020-01-26
显示第 1 条-第 6 条,共 6 条
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: number | undefined;
    owner: string;
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

export default function TableDemo() {
    const columns: ColumnProps<Row>[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 400,
            render: (text) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
        },
        {
            title: '大小',
            dataIndex: 'size',
            sorter: (r1, r2, order) => {
                const a = r1.size;
                const b = r2.size;
                if (typeof a === 'number' && typeof b === 'number') {
                    return a - b; // 数字正常比较大小
                } else if (typeof a === 'undefined') {
                    return order === 'ascend' ? 1 : -1; // undefined 在后面
                } else if (typeof b === 'undefined') {
                    return order === 'ascend' ? -1 : 1; // undefined 在后面
                } else {
                    return 0; // 保持原来的顺序
                }
            },
            showSortTip: true,
            render: (text) => (text ? `${text} KB` : '未知'),
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            render: (value) => {
                return dateFns.format(new Date(value), 'yyyy-MM-dd');
            },
        },
    ];

    const figmaIconUrl = '/assets/placeholder/app.svg';
    const docIconUrl = '/assets/docs-icon.png';

    const dataSource: Row[] = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: figmaIconUrl,
            size: 3,
            owner: '姜鹏志',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: docIconUrl,
            size: undefined,
            owner: '郝宣',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档 3',
            nameIconSrc: docIconUrl,
            size: 1,
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
        {
            key: '4',
            name: '设计文档 4',
            nameIconSrc: docIconUrl,
            size: 5,
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
        {
            key: '5',
            name: '设计文档 5',
            nameIconSrc: docIconUrl,
            size: undefined,
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
        {
            key: '6',
            name: '设计文档 6',
            nameIconSrc: docIconUrl,
            size: 2,
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    return <Table columns={columns} dataSource={dataSource} />;
}

Custom Header Filtering

If you need to display the filter input box in the table header, you can pass ReactNode in the title and use it with filteredValue.

标题
交付状态所有者
Semi Design 设计稿0.fig
0 KB
已交付
姜姜鹏志
2024-01-25
Semi D2C 首页1.fig
5 KB
待评审
郝郝宣
2024-01-30
Semi Design 设计稿2.fig
10 KB
已交付
姜姜鹏志
2024-02-04
Semi D2C 首页3.fig
15 KB
待评审
郝郝宣
2024-02-09
Semi Design 设计稿4.fig
20 KB
已交付
姜姜鹏志
2024-02-14
Semi D2C 首页5.fig
25 KB
待评审
郝郝宣
2024-02-19
Semi Design 设计稿6.fig
30 KB
已交付
姜姜鹏志
2024-02-24
Semi D2C 首页7.fig
35 KB
待评审
郝郝宣
2024-02-29
Semi Design 设计稿8.fig
40 KB
已交付
姜姜鹏志
2024-03-05
Semi D2C 首页9.fig
45 KB
待评审
郝郝宣
2024-03-10
显示第 1 条-第 10 条,共 46 条
Code
import { useState, useRef, type CompositionEvent } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Input, Space, Tag, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

const getData = () => {
    const data = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            status: isSemiDesign ? 'success' : 'wait',
            updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);
    const [filteredValue, setFilteredValue] = useState<string[]>([]);
    const compositionRef = useRef({ isComposition: false });

    const figmaIconUrl = '/assets/placeholder/app.svg';

    const handleChange = (value: string) => {
        if (compositionRef.current.isComposition) {
            return;
        }
        const newFilteredValue = value ? [value] : [];
        setFilteredValue(newFilteredValue);
    };
    const handleCompositionStart = () => {
        compositionRef.current.isComposition = true;
    };

    const handleCompositionEnd = (event: CompositionEvent<HTMLInputElement>) => {
        compositionRef.current.isComposition = false;
        const value = (event.target as HTMLInputElement).value;
        const newFilteredValue = value ? [value] : [];
        setFilteredValue(newFilteredValue);
    };

    const columns: ColumnProps[] = [
        {
            title: (
                <Space>
                    <span>标题</span>
                    <Input placeholder="请输入筛选值" style={{ width: 200 }} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onChange={handleChange} showClear />
                </Space>
            ),
            dataIndex: 'name',
            width: 400,
            render: (text) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
            onFilter: (value, record) => record.name.includes(value),
            filteredValue,
        },
        {
            title: '大小',
            dataIndex: 'size',
            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
            render: (text) => `${text} KB`,
        },
        {
            title: '交付状态',
            dataIndex: 'status',
            render: (text: 'success' | 'pending' | 'wait') => {
                const tagConfig = {
                    success: { color: 'green', prefixIcon: <IconTickCircle />, text: '已交付' },
                    pending: { color: 'pink', prefixIcon: <IconClear />, text: '已延期' },
                    wait: { color: 'cyan', prefixIcon: <IconComment />, text: '待评审' },
                } as const;
                const tagProps = tagConfig[text];
                return (
                    <Tag shape="circle" {...tagProps} style={{ userSelect: 'text' }}>
                        {tagProps.text}
                    </Tag>
                );
            },
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
            render: (value) => {
                return dateFns.format(new Date(value), 'yyyy-MM-dd');
            },
        },
    ];

    return <Table columns={columns} dataSource={dataSource} />;
}

Custom Filter Rendering

Use renderFilterDropdown to customize the render filter panel.

You can call setTempFilteredValue to store the filter value when the user enters the filter value, and call confirm to trigger the actual filtering after the filter value is entered. You can also filter directly through confirm({ filteredValue }).

The reason for setting tempFilteredValue is that in scenarios where temporary filtered values need to be stored, there is no need to declare a state to save this temporary filtered value.

type RenderFilterDropdown = (props: RenderFilterDropdownProps) => React.ReactNode;
interface RenderFilterDropdownProps {
    /** Temporary filter value, the initial value is `filteredValue` or `defaultFilteredValue` */
    tempFilteredValue: any[];
    /** Set temporary filter value */
    setTempFilteredValue: (tempFilteredValue: any[]) => void;
    /** `confirm` will assign `tempFilteredValue` to `filteredValue` by default and trigger the `onChange` event. You can also set the filter value directly by passing in `filteredValue` */
    confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
    /** Clear filter values and temporary filter values */
    clear: (props?: { closeDropdown?: boolean }) => void;
    /** Close dropdown */
    close: () => void;
    /** Restore the temporary filter value to the applied filter value */
    reset: () => void;
    /** Filter configuration items, do not pass if not required */
    filters?: RenderDropdownProps['filters'];
}
标题
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-08-02
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2019-08-22
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2019-09-01
Semi Design 设计稿10.fig
50 KB
姜姜鹏志
2019-09-11
Semi Design 设计稿12.fig
60 KB
姜姜鹏志
2019-09-21
Semi Design 设计稿14.fig
70 KB
姜姜鹏志
2019-10-01
Semi Design 设计稿16.fig
80 KB
姜姜鹏志
2019-10-11
Semi Design 设计稿18.fig
90 KB
姜姜鹏志
2019-10-21
显示第 1 条-第 10 条,共 23 条
Code
import { useState, useRef } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Input, Button, Space, type InputHandle, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

const getData = () => {
    const data = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);
    const inputRef = useRef<InputHandle>(null);

    const figmaIconUrl = '/assets/placeholder/app.svg';

    const columns: ColumnProps[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 400,
            render: (text) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
            onFilter: (value, record) => record.name.includes(value),
            renderFilterDropdown: (props) => {
                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;

                const handleChange = (value: string) => {
                    const filteredValue = value ? [value] : [];
                    setTempFilteredValue(filteredValue);
                };

                return (
                    <Space vertical align="start" style={{ padding: 8 }}>
                        <Input ref={inputRef} value={tempFilteredValue[0]} onChange={handleChange} />
                        <Space>
                            <Button onClick={() => confirm({ closeDropdown: true })}>筛选 + 关闭</Button>
                            <Button onClick={() => clear({ closeDropdown: true })}>清除 + 关闭</Button>
                            <Button onClick={() => close()}>直接关闭</Button>
                        </Space>
                    </Space>
                );
            },
            onFilterDropdownVisibleChange: () => {
                if (inputRef.current && inputRef.current.focus) {
                    inputRef.current.focus();
                }
            },
        },
        {
            title: '大小',
            dataIndex: 'size',
            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
            render: (text) => `${text} KB`,
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
            onFilter: (value, record) => record.owner.includes(value),
            defaultFilteredValue: ['姜鹏志'],
            renderFilterDropdown: (props) => {
                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;

                const handleChange = (value: string) => {
                    if (value) {
                        setTempFilteredValue([value]);
                    } else {
                        setTempFilteredValue([]);
                    }
                };

                return (
                    <Space vertical align="start" style={{ padding: 8 }}>
                        <Input value={tempFilteredValue[0]} onChange={handleChange} />
                        <Space>
                            <Button onClick={() => confirm({ closeDropdown: false })}>筛选后不关闭</Button>
                            <Button onClick={() => clear({ closeDropdown: false })}>清除后不关闭</Button>
                            <Button onClick={() => close()}>直接关闭</Button>
                        </Space>
                    </Space>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
            render: (value) => {
                return dateFns.format(new Date(value), 'yyyy-MM-dd');
            },
        },
    ];

    return <Table columns={columns} dataSource={dataSource} />;
}

Filter Confirm Mode

By setting filterConfirmMode='confirm', the filter dropdown panel can support confirm mode. In this mode:

  • Clicking filter items will not take effect immediately, but will be temporarily stored
  • The bottom of the dropdown panel will show "Confirm" and "Reset" buttons
  • Clicking the "Confirm" button will apply the filter conditions and close the dropdown panel
  • Clicking the "Reset" button will restore to the initial state when the dropdown was opened (will not close the panel)

This feature is suitable for scenarios where multiple filter conditions need to be selected before applying them at once, avoiding triggering filtering on every click.

标题
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2024-01-25
Semi Pro 首页1.fig
5 KB
郝郝宣
2024-01-30
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2024-02-04
Semi Pro 首页3.fig
15 KB
郝郝宣
2024-02-09
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2024-02-14
Semi Pro 首页5.fig
25 KB
郝郝宣
2024-02-19
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2024-02-24
Semi Pro 首页7.fig
35 KB
郝郝宣
2024-02-29
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2024-03-05
Semi Pro 首页9.fig
45 KB
郝郝宣
2024-03-10
显示第 1 条-第 10 条,共 20 条
Code
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 20; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 首页${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);

    const figmaIconUrl = '/assets/placeholder/app.svg';

    const columns: ColumnProps<Row>[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 300,
            render: (text) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
            filters: [
                {
                    text: 'Semi Design 设计稿',
                    value: 'Semi Design',
                },
                {
                    text: 'Semi Pro 设计稿',
                    value: 'Semi Pro',
                },
            ],
            onFilter: (value, record) => record.name.includes(value),
            filterMultiple: true,
            filterConfirmMode: 'confirm',
        },
        {
            title: '大小',
            dataIndex: 'size',
            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
            render: (text) => `${text} KB`,
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            filters: [
                {
                    text: '姜鹏志',
                    value: '姜鹏志',
                },
                {
                    text: '郝宣',
                    value: '郝宣',
                },
            ],
            onFilter: (value, record) => record.owner.includes(value),
            filterMultiple: true,
            filterConfirmMode: 'confirm',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
            render: (value) => {
                return dateFns.format(new Date(value), 'yyyy-MM-dd');
            },
        },
    ];

    return <Table columns={columns} dataSource={dataSource} />;
}

Custom Filter Item Rendering

It is supported to pass in renderFilterDropdownItem to customize the rendering method of each filter item.

This method accepts an Object input parameter, and the meaning of each attribute is as follows:

  • onChange: e => void current filter item selection/reverse selection event;
  • text: ReactNode copy of the current screening item;
  • value: any the value of the current filter item;
  • checked: boolean whether the current filter item has been selected;
  • filteredValue: any[] all currently filtered values;
  • level: number is the level of the current filter item, if it is a nested filter item, the value will be >= 1;
  • filterMultiple: boolean whether the current filter item is multi-select.
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig
25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig
35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig
45 KB
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Dropdown, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
        renderFilterDropdownItem: ({ text, checked, onChange }) => (
            <Dropdown.Item onClick={onChange} active={checked}>
                {text}
            </Dropdown.Item>
        ),
        filterDropdownProps: {
            showTick: true,
        },
        sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(getData);

    const rowSelection = useMemo<RowSelectionProps<Row>>(
        () => ({
            onChange: (selectedRowKeys, selectedRows) => {
                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
            },
            getCheckboxProps: (record) => ({
                disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
                name: record.name,
            }),
        }),
        []
    );
    const scroll = useMemo(() => ({ y: 300 }), []);

    return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}

A Table That Can Be Expanded

A Common Table That Can Be Expanded

If you need to render a table that can be expanded, in addition to the need to pass expandedRowRender In addition to this method, you must specify rowKey(by default key), Table will be based on rowKey Gets a row unique identifier.

  • If rowKey is a Function, the result of rowKey(record) is taken as the row unique ID
  • If rowKey is a string type, you take record[rowKey] as the row unique ID
标题大小所有者更新日期
Semi Design 设计稿.fig2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿2M
郝郝宣
2020-01-17 05:31
设计文档34KB
ZZoey Edwards
2020-01-26 11:01
Code
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        width: 500,
        dataIndex: 'name',
        render: (text, record) => {
            return (
                <span>
                    <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                    {text}
                </span>
            );
        },
    },
    {
        title: '大小',
        dataIndex: 'size',
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
    },
    {
        title: '',
        dataIndex: 'operate',
        render: () => {
            return <IconMore />;
        },
    },
];

const data: Row[] = [
    {
        key: '1',
        name: 'Semi Design 设计稿.fig',
        nameIconSrc: '/assets/placeholder/app.svg',
        size: '2M',
        owner: '姜鹏志',
        updateTime: '2020-02-02 05:13',
        avatarBg: 'grey',
    },
    {
        key: '2',
        name: 'Semi Design 分享演示文稿',
        nameIconSrc: '/assets/docs-icon.png',
        size: '2M',
        owner: '郝宣',
        updateTime: '2020-01-17 05:31',
        avatarBg: 'red',
    },
    {
        key: '3',
        name: '设计文档',
        nameIconSrc: '/assets/docs-icon.png',
        size: '34KB',
        owner: 'Zoey Edwards',
        updateTime: '2020-01-26 11:01',
        avatarBg: 'light-blue',
    },
];

const expandData: Record<number, { key: string; value: ReactNode }[]> = {
    0: [
        { key: '实际用户数量', value: '1,480,000' },
        { key: '7 天留存', value: '98%' },
        { key: '安全等级', value: '3 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>设计</Tag> },
        { key: '认证状态', value: '未认证' },
    ],
    1: [
        { key: '实际用户数量', value: '2,480,000' },
        { key: '7 天留存', value: '90%' },
        { key: '安全等级', value: '1 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>模板</Tag> },
        { key: '认证状态', value: '已认证' },
    ],
    2: [
        { key: '实际用户数量', value: '2,920,000' },
        { key: '7 天留存', value: '98%' },
        { key: '安全等级', value: '2 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>文档</Tag> },
        { key: '认证状态', value: '已认证' },
    ],
};

export default function TableDemo() {
    const expandRowRender: ExpandedRowRender<Row> = (_record, index) => {
        return <Descriptions align="justify" data={expandData[index]} />;
    };

    const rowSelection: RowSelectionProps<Row> = {
        getCheckboxProps: (record) => ({
            disabled: record.name === '设计文档', // 这一行不可勾选
            name: record.name,
        }),
        onSelect: (record, selected) => {
            console.log(`select row: ${selected}`, record);
        },
        onSelectAll: (selected, selectedRows) => {
            console.log(`select all rows: ${selected}`, selectedRows);
        },
        onChange: (selectedRowKeys, selectedRows) => {
            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
        },
    };

    return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowSelection={rowSelection} pagination={false} />;
}

The Unfold Button Is Rendered In a Separated Column

By default, the expansion button will be rendered in the same cell as the first copy, and you can set hideExpandedColumn to false to render as a separate column:

展开标题大小所有者更新日期
Semi Design 设计稿.fig2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿2M
郝郝宣
2020-01-17 05:31
设计文档34KB
ZZoey Edwards
2020-01-26 11:01
Code
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        width: 500,
        dataIndex: 'name',
        render: (text, record) => {
            return (
                <span>
                    <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                    {text}
                </span>
            );
        },
    },
    {
        title: '大小',
        dataIndex: 'size',
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
    },
    {
        title: '',
        dataIndex: 'operate',
        render: () => {
            return <IconMore />;
        },
    },
];

const data: Row[] = [
    {
        key: '1',
        name: 'Semi Design 设计稿.fig',
        nameIconSrc: '/assets/placeholder/app.svg',
        size: '2M',
        owner: '姜鹏志',
        updateTime: '2020-02-02 05:13',
        avatarBg: 'grey',
    },
    {
        key: '2',
        name: 'Semi Design 分享演示文稿',
        nameIconSrc: '/assets/docs-icon.png',
        size: '2M',
        owner: '郝宣',
        updateTime: '2020-01-17 05:31',
        avatarBg: 'red',
    },
    {
        key: '3',
        name: '设计文档',
        nameIconSrc: '/assets/docs-icon.png',
        size: '34KB',
        owner: 'Zoey Edwards',
        updateTime: '2020-01-26 11:01',
        avatarBg: 'light-blue',
    },
];

const expandData: Record<number, { key: string; value: ReactNode }[]> = {
    0: [
        { key: '实际用户数量', value: '1,480,000' },
        { key: '7 天留存', value: '98%' },
        { key: '安全等级', value: '3 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>设计</Tag> },
        { key: '认证状态', value: '未认证' },
    ],
    1: [
        { key: '实际用户数量', value: '2,480,000' },
        { key: '7 天留存', value: '90%' },
        { key: '安全等级', value: '1 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>模板</Tag> },
        { key: '认证状态', value: '已认证' },
    ],
    2: [
        { key: '实际用户数量', value: '2,920,000' },
        { key: '7 天留存', value: '98%' },
        { key: '安全等级', value: '2 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>文档</Tag> },
        { key: '认证状态', value: '已认证' },
    ],
};

export default function TableDemo() {
    const expandRowRender: ExpandedRowRender<Row> = (_record, index) => {
        return <Descriptions align="justify" data={expandData[index]} />;
    };

    const rowSelection: RowSelectionProps<Row> = {
        getCheckboxProps: (record) => ({
            disabled: record.name === '设计文档', // 这一行不可勾选
            name: record.name,
        }),
        onSelect: (record, selected) => {
            console.log(`select row: ${selected}`, record);
        },
        onSelectAll: (selected, selectedRows) => {
            console.log(`select all rows: ${selected}`, selectedRows);
        },
        onChange: (selectedRowKeys, selectedRows) => {
            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
        },
    };

    return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} hideExpandedColumn={false} rowSelection={rowSelection} pagination={false} />;
}

Turn Off Rendering of Expandable Button of a Row

You can pass the row Expandable method, enter the reference as record, and determine whether the return value is false to close the rendering of the expandable button on a row.

展开标题大小所有者更新日期
Semi Design 设计稿.fig2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿2M
郝郝宣
2020-01-17 05:31
设计文档34KB
ZZoey Edwards
2020-01-26 11:01
Code
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        width: 500,
        dataIndex: 'name',
        render: (text, record) => {
            return (
                <span>
                    <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                    {text}
                </span>
            );
        },
    },
    {
        title: '大小',
        dataIndex: 'size',
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
    },
    {
        title: '',
        dataIndex: 'operate',
        render: () => {
            return <IconMore />;
        },
    },
];

const data: Row[] = [
    {
        key: '1',
        name: 'Semi Design 设计稿.fig',
        nameIconSrc: '/assets/placeholder/app.svg',
        size: '2M',
        owner: '姜鹏志',
        updateTime: '2020-02-02 05:13',
        avatarBg: 'grey',
    },
    {
        key: '2',
        name: 'Semi Design 分享演示文稿',
        nameIconSrc: '/assets/docs-icon.png',
        size: '2M',
        owner: '郝宣',
        updateTime: '2020-01-17 05:31',
        avatarBg: 'red',
    },
    {
        key: '3',
        name: '设计文档',
        nameIconSrc: '/assets/docs-icon.png',
        size: '34KB',
        owner: 'Zoey Edwards',
        updateTime: '2020-01-26 11:01',
        avatarBg: 'light-blue',
    },
];

const expandData: Record<number, { key: string; value: ReactNode }[]> = {
    0: [
        { key: '实际用户数量', value: '1,480,000' },
        { key: '7 天留存', value: '98%' },
        { key: '安全等级', value: '3 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>设计</Tag> },
        { key: '认证状态', value: '未认证' },
    ],
    1: [
        { key: '实际用户数量', value: '2,480,000' },
        { key: '7 天留存', value: '90%' },
        { key: '安全等级', value: '1 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>模板</Tag> },
        { key: '认证状态', value: '已认证' },
    ],
    2: [
        { key: '实际用户数量', value: '2,920,000' },
        { key: '7 天留存', value: '98%' },
        { key: '安全等级', value: '2 级' },
        { key: '垂类标签', value: <Tag style={{ margin: 0 }}>文档</Tag> },
        { key: '认证状态', value: '已认证' },
    ],
};

export default function TableDemo() {
    const expandRowRender: ExpandedRowRender<Row> = (_record, index) => {
        return <Descriptions align="justify" data={expandData[index]} />;
    };

    const rowSelection: RowSelectionProps<Row> = {
        getCheckboxProps: (record) => ({
            disabled: record.name === '设计文档', // 这一行不可勾选
            name: record.name,
        }),
        onSelect: (record, selected) => {
            console.log(`select row: ${selected}`, record);
        },
        onSelectAll: (selected, selectedRows) => {
            console.log(`select all rows: ${selected}`, selectedRows);
        },
        onChange: (selectedRowKeys, selectedRows) => {
            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
        },
    };

    return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowExpandable={(record) => record.name !== '设计文档'} hideExpandedColumn={false} rowSelection={rowSelection} pagination={false} />;
}

Tree Data Display

The table supports the display of tree data and is automatically displayed as a tree table when there are children fields in the data. If you do not need or use other fields, you can configure it with the childrenRecordName. In addition, you can control the indent width of each layer by setting indent Size.

Note: Be sure to provide a "key" for each row of data that is different from the other row values, or use the rowKey parameter to specify a property name as the primary key.

Simple example of tree data

Key名称数据类型描述默认值
videos_info视频信息Object 对象视频的元信息无
text_info文本信息Object 对象视频的元信息无
显示第 1 条-第 2 条,共 2 条
Code
import { Table } from '@fibrous/semi-ui-react';

export default function TableDemo() {
    const columns = [
        {
            title: 'Key',
            dataIndex: 'dataKey',
            key: 'dataKey',
        },
        {
            title: '名称',
            dataIndex: 'name',
            key: 'name',
            width: 200,
        },
        {
            title: '数据类型',
            dataIndex: 'type',
            key: 'type',
            width: 400,
        },
        {
            title: '描述',
            dataIndex: 'description',
            key: 'description',
        },
        {
            title: '默认值',
            dataIndex: 'default',
            key: 'default',
            width: 100,
        },
    ];

    const data = [
        {
            key: 1,
            dataKey: 'videos_info',
            name: '视频信息',
            type: 'Object 对象',
            description: '视频的元信息',
            default: '无',
            children: [
                {
                    key: 11,
                    dataKey: 'status',
                    name: '视频状态',
                    type: 'Enum <Integer> 枚举',
                    description: '视频的可见、推荐状态',
                    default: '1',
                },
                {
                    key: 12,
                    dataKey: 'vid',
                    name: '视频 ID',
                    type: 'String 字符串',
                    description: '标识视频的唯一 ID',
                    default: '无',
                    children: [
                        {
                            dataKey: 'video_url',
                            name: '视频地址',
                            type: 'String 字符串',
                            description: '视频的唯一链接',
                            default: '无',
                        },
                    ],
                },
            ],
        },
        {
            key: 2,
            dataKey: 'text_info',
            name: '文本信息',
            type: 'Object 对象',
            description: '视频的元信息',
            default: '无',
            children: [
                {
                    key: 21,
                    dataKey: 'title',
                    name: '视频标题',
                    type: 'String 字符串',
                    description: '视频的标题',
                    default: '无',
                },
                {
                    key: 22,
                    dataKey: 'video_description',
                    name: '视频描述',
                    type: 'String 字符串',
                    description: '视频的描述',
                    default: '无',
                },
            ],
        },
    ];

    return <Table columns={columns} defaultExpandAllRows dataSource={data} />;
}

Rows of Interchangeable Tree Data

You can do row switching by changing the order of the dataSource elements.

Key名称数据类型描述默认值
videos_info视频信息Object 对象视频的元信息无
status视频状态Enum <Integer> 枚举视频的可见、推荐状态1
vid视频 IDString 字符串标识视频的唯一 ID无
text_info文本信息Object 对象视频的元信息无
title视频标题String 字符串视频的标题无
video_description视频描述String 字符串视频的描述无
显示第 1 条-第 2 条,共 2 条
Code
import { useState, type ComponentProps } from 'react';
import { IconArrowUp, IconArrowDown } from '@fibrous/semi-icons-react';
import { Table, Button, type ColumnProps } from '@fibrous/semi-ui-react';

type TreeRow = {
    key?: number;
    dataKey: string;
    name: string;
    type: string;
    description: string;
    default: string;
    children?: TreeRow[];
};

const raw: TreeRow[] = [
    {
        key: 1,
        dataKey: 'videos_info',
        name: '视频信息',
        type: 'Object 对象',
        description: '视频的元信息',
        default: '无',
        children: [
            {
                key: 11,
                dataKey: 'status',
                name: '视频状态',
                type: 'Enum <Integer> 枚举',
                description: '视频的可见、推荐状态',
                default: '1',
            },
            {
                key: 12,
                dataKey: 'vid',
                name: '视频 ID',
                type: 'String 字符串',
                description: '标识视频的唯一 ID',
                default: '无',
                children: [
                    {
                        dataKey: 'video_url',
                        name: '视频地址',
                        type: 'String 字符串',
                        description: '视频的唯一链接',
                        default: '无',
                    },
                ],
            },
        ],
    },
    {
        key: 2,
        dataKey: 'text_info',
        name: '文本信息',
        type: 'Object 对象',
        description: '视频的元信息',
        default: '无',
        children: [
            {
                key: 21,
                dataKey: 'title',
                name: '视频标题',
                type: 'String 字符串',
                description: '视频的标题',
                default: '无',
            },
            {
                key: 22,
                dataKey: 'video_description',
                name: '视频描述',
                type: 'String 字符串',
                description: '视频的描述',
                default: '无',
            },
        ],
    },
];

const rowKey = 'key';
const childrenRecordName = 'children';

export default function TableDemo() {
    const [expandedRowKeys, setExpandedRowKeys] = useState([1, 2]);
    const [data, setData] = useState(raw);

    const switchRecord = (key1?: number, key2?: number) => {
        const newData = [...data];

        if (key1 != null && key2 != null) {
            const item1 = findRecordByKey(key1, newData);
            const item2 = findRecordByKey(key2, newData);

            const copiedItem1 = { ...item1 };
            const copiedItem2 = { ...item2 };

            coverRecord(item1, copiedItem2);
            coverRecord(item2, copiedItem1);

            setData(newData);
        }
    };

    const findRecordByKey = (key: number, data: TreeRow[]): TreeRow | undefined => {
        if (Array.isArray(data) && data.length && key != null) {
            for (let item of data) {
                if (item[rowKey] === key) {
                    return item;
                }

                const children = item[childrenRecordName];
                if (Array.isArray(children) && children.length) {
                    const item = findRecordByKey(key, children);

                    if (item != null) {
                        return item;
                    }
                }
            }
        }
    };

    const coverRecord = (obj?: Record<string, unknown>, srcObj?: Record<string, unknown>) => {
        if (obj && typeof obj === 'object' && srcObj && typeof srcObj === 'object') {
            const srcKeys = Object.keys(srcObj);
            const copied = { ...srcObj };

            Object.assign(obj, copied);

            Object.keys(obj).forEach((key) => {
                if (!srcKeys.includes(key)) {
                    delete obj[key];
                }
            });
        }

        return obj;
    };

    const getSameLevelRecords = (key: number | undefined, data: TreeRow[] = []): TreeRow[] => {
        if (key != null && Array.isArray(data) && data.length) {
            if (data.find((item) => item[rowKey] === key)) {
                return data;
            }
            for (let item of data) {
                const records = getSameLevelRecords(key, item[childrenRecordName]);

                if (records.length) {
                    return records;
                }
            }
        }
        return [];
    };

    const columns: ColumnProps<TreeRow>[] = [
        {
            title: 'Key',
            dataIndex: 'dataKey',
            key: 'dataKey',
        },
        {
            title: '名称',
            dataIndex: 'name',
            key: 'name',
            width: 200,
        },
        {
            title: '数据类型',
            dataIndex: 'type',
            key: 'type',
        },
        {
            title: '描述',
            dataIndex: 'description',
            key: 'description',
        },
        {
            title: '默认值',
            dataIndex: 'default',
            key: 'default',
            width: 100,
        },
        {
            key: 'operation',
            render: (record: TreeRow) => {
                const records = getSameLevelRecords(record[rowKey], data);
                const index = records.findIndex((item) => item[rowKey] === record[rowKey]);
                const upProps: ComponentProps<typeof Button> = {};
                const downProps: ComponentProps<typeof Button> = {};

                if (index > 0) {
                    const upRow = records[index - 1];
                    upProps.onClick = () => switchRecord(record[rowKey], upRow[rowKey]);
                } else {
                    upProps.disabled = true;
                }

                if (index < records.length - 1) {
                    const downRow = records[index + 1];
                    downProps.onClick = () => switchRecord(record[rowKey], downRow[rowKey]);
                } else {
                    downProps.disabled = true;
                }

                return (
                    <>
                        <Button aria-label="上移" icon={<IconArrowUp />} {...upProps} />
                        <Button aria-label="下移" icon={<IconArrowDown />} {...downProps} />
                    </>
                );
            },
        },
    ];

    return <Table columns={columns} dataSource={data} rowKey={rowKey} childrenRecordName={childrenRecordName} expandedRowKeys={expandedRowKeys} onExpandedRowsChange={(rows = []) => setExpandedRowKeys(rows.flatMap((item) => ('key' in item && item.key !== undefined ? [item.key] : [])))} />;
}

Tree Selection

By default, the row selection of the table is independent. You can simulate a tree selection by defining selectedRowKeys.

Key名称数据类型描述默认值
videos_info视频信息Object 对象视频的元信息无
text_info文本信息Object 对象视频的元信息无
Code
import { useMemo, useState, useCallback } from 'react';
import { union, pull } from 'es-toolkit';
import { Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

type TreeRow = {
    key: number;
    dataKey: string;
    name: string;
    type: string;
    description: string;
    default: string;
    children?: TreeRow[];
};
type RowKey = string | number;

const childrenRecordName = 'children';
const rowKey = 'key';
const getKey = (record: TreeRow): RowKey => record[rowKey];

const traverse = (data: TreeRow[], res: RowKey[]) => {
    for (let record of data) {
        const children = record?.children;
        res.push(getKey(record));
        if (Array.isArray(children)) {
            traverse(children, res);
        }
    }
};

const getAllRowKeys = (data: TreeRow[]) => {
    const allRowKeys: RowKey[] = [];
    traverse(data, allRowKeys);
    return allRowKeys;
};

const findShouldSelectRowKeys = (record: TreeRow, selected: boolean, selectedRowKeys: RowKey[]) => {
    let shouldSelectRowKeys: RowKey[];
    const children = record.children;
    const childrenRowKeys: RowKey[] = [];
    if (Array.isArray(children)) {
        traverse(children, childrenRowKeys);
    }

    const key = getKey(record);
    if (!selected) {
        shouldSelectRowKeys = [...selectedRowKeys];
        pull(shouldSelectRowKeys, [key, ...childrenRowKeys]);
    } else {
        shouldSelectRowKeys = union(selectedRowKeys, [key, ...childrenRowKeys]);
    }
    return shouldSelectRowKeys;
};

export default function TableDemo() {
    const [selectedRowKeys, setSelectedRowKeys] = useState<RowKey[]>([]);

    const columns = useMemo<ColumnProps<TreeRow>[]>(
        () => [
            {
                title: 'Key',
                dataIndex: 'dataKey',
                key: 'dataKey',
            },
            {
                title: '名称',
                dataIndex: 'name',
                key: 'name',
                width: 200,
            },
            {
                title: '数据类型',
                dataIndex: 'type',
                key: 'type',
                width: 400,
            },
            {
                title: '描述',
                dataIndex: 'description',
                key: 'description',
            },
            {
                title: '默认值',
                dataIndex: 'default',
                key: 'default',
                width: 100,
            },
        ],
        []
    );

    const data = useMemo<TreeRow[]>(
        () => [
            {
                key: 1,
                dataKey: 'videos_info',
                name: '视频信息',
                type: 'Object 对象',
                description: '视频的元信息',
                default: '无',
                children: [
                    {
                        key: 11,
                        dataKey: 'status',
                        name: '视频状态',
                        type: 'Enum <Integer> 枚举',
                        description: '视频的可见、推荐状态',
                        default: '1',
                    },
                    {
                        key: 12,
                        dataKey: 'vid',
                        name: '视频 ID',
                        type: 'String 字符串',
                        description: '标识视频的唯一 ID',
                        default: '无',
                        children: [
                            {
                                key: 121,
                                dataKey: 'video_url',
                                name: '视频地址',
                                type: 'String 字符串',
                                description: '视频的唯一链接',
                                default: '无',
                            },
                        ],
                    },
                ],
            },
            {
                key: 2,
                dataKey: 'text_info',
                name: '文本信息',
                type: 'Object 对象',
                description: '视频的元信息',
                default: '无',
                children: [
                    {
                        key: 21,
                        dataKey: 'title',
                        name: '视频标题',
                        type: 'String 字符串',
                        description: '视频的标题',
                        default: '无',
                    },
                    {
                        key: 22,
                        dataKey: 'video_description',
                        name: '视频描述',
                        type: 'String 字符串',
                        description: '视频的描述',
                        default: '无',
                    },
                ],
            },
        ],
        []
    );

    const doSelect = useCallback(
        (record: TreeRow, selected: boolean) => {
            const rowKeys = findShouldSelectRowKeys(record, selected, selectedRowKeys);
            setSelectedRowKeys(rowKeys);
            console.log('select', record, rowKeys);
        },
        [selectedRowKeys]
    );

    const doSelectAll = useCallback(
        (selected: boolean) => {
            let rowKeys: RowKey[] = [];
            if (selected) {
                rowKeys = getAllRowKeys(data);
            }
            setSelectedRowKeys(rowKeys);
        },
        [data]
    );

    const rowSelection = useMemo<RowSelectionProps<TreeRow>>(
        () => ({
            selectedRowKeys,
            onSelect: doSelect,
            onSelectAll: doSelectAll,
        }),
        [selectedRowKeys, doSelect, doSelectAll]
    );

    return <Table columns={columns} rowKey={rowKey} childrenRecordName={childrenRecordName} rowSelection={rowSelection} dataSource={data} pagination={false} />;
}

Tree Selection Association (checkRelation)

By setting rowSelection.checkRelation to 'related', you can enable parent-child node selection association. Selecting a parent node automatically selects all child nodes, and selecting a child node affects the parent node's state (checked/half-checked/unchecked).

Key名称数据类型描述
videos_info视频信息Object视频的元信息
text_info文本信息Object文本的元信息
Code
import { useMemo, useState } from 'react';
import { Table, type RowSelectionProps } from '@fibrous/semi-ui-react';

export default function TableDemo() {
    const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);

    const columns = useMemo(
        () => [
            {
                title: 'Key',
                dataIndex: 'dataKey',
                key: 'dataKey',
            },
            {
                title: '名称',
                dataIndex: 'name',
                key: 'name',
                width: 200,
            },
            {
                title: '数据类型',
                dataIndex: 'type',
                key: 'type',
                width: 200,
            },
            {
                title: '描述',
                dataIndex: 'description',
                key: 'description',
            },
        ],
        []
    );

    const data = useMemo(
        () => [
            {
                key: '1',
                dataKey: 'videos_info',
                name: '视频信息',
                type: 'Object',
                description: '视频的元信息',
                children: [
                    {
                        key: '1-1',
                        dataKey: 'status',
                        name: '视频状态',
                        type: 'Enum',
                        description: '视频的可见状态',
                    },
                    {
                        key: '1-2',
                        dataKey: 'vid',
                        name: '视频 ID',
                        type: 'String',
                        description: '标识视频的唯一 ID',
                        children: [
                            {
                                key: '1-2-1',
                                dataKey: 'video_url',
                                name: '视频地址',
                                type: 'String',
                                description: '视频的唯一链接',
                            },
                        ],
                    },
                ],
            },
            {
                key: '2',
                dataKey: 'text_info',
                name: '文本信息',
                type: 'Object',
                description: '文本的元信息',
                children: [
                    {
                        key: '2-1',
                        dataKey: 'title',
                        name: '标题',
                        type: 'String',
                        description: '文本标题',
                    },
                    {
                        key: '2-2',
                        dataKey: 'description',
                        name: '描述',
                        type: 'String',
                        description: '文本描述',
                    },
                ],
            },
        ],
        []
    );

    const rowSelection = useMemo(
        (): RowSelectionProps<Record<string, unknown>> => ({
            selectedRowKeys,
            onChange: (selectedRowKeys = []) => {
                setSelectedRowKeys(selectedRowKeys);
            },
            checkRelation: 'related',
        }),
        [selectedRowKeys]
    );

    return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={false} />;
}

Custom Row or Cell Events and Properties

  • The incoming onRow / onHeaderRow can define the native event or property of a table or table header line.
  • The incoming Column.onCell / Column.onHeaderCell can define a table or header cell native event or property.

In principle, the properties or events supported on tr / td / th can be defined. For example:

  • The tr at the head of the watch defines onMouseEnter / onMouseLeave
  • The tr of the table defines className
  • The third line of the table defines onClick.
标题
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig
25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig
35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig
45 KB
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useMemo, useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnHeaderRow, type OnRow } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

export default function TableDemo() {
    const columns = useMemo<ColumnProps<Row>[]>(
        () => [
            {
                title: '标题',
                dataIndex: 'name',
                width: 400,
                render: (text) => {
                    return (
                        <div>
                            <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                            {text}
                        </div>
                    );
                },
                filters: [
                    {
                        text: 'Semi Design 设计稿',
                        value: 'Semi Design 设计稿',
                    },
                    {
                        text: 'Semi D2C 设计稿',
                        value: 'Semi D2C 设计稿',
                    },
                ],
                onFilter: (value, record) => record.name.includes(value),
            },
            {
                title: '大小',
                dataIndex: 'size',
                sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
                render: (text) => `${text} KB`,
            },
            {
                title: '所有者',
                dataIndex: 'owner',
                render: (text, record) => {
                    return (
                        <div>
                            <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                                {typeof text === 'string' && text.slice(0, 1)}
                            </Avatar>
                            {text}
                        </div>
                    );
                },
            },
            {
                title: '更新日期',
                dataIndex: 'updateTime',
                sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
                render: (value) => {
                    return dateFns.format(new Date(value), 'yyyy-MM-dd');
                },
            },
        ],
        []
    );

    const [data] = useState(() => {
        const _data: Row[] = [];
        for (let i = 0; i < 46; i++) {
            const isSemiDesign = i % 2 === 0;
            const randomNumber = (i * 1000) % 199;
            _data.push({
                key: '' + i,
                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
                owner: isSemiDesign ? '姜鹏志' : '郝宣',
                size: randomNumber,
                updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
                avatarBg: isSemiDesign ? 'grey' : 'red',
            });
        }
        return _data;
    });

    const onRow = useMemo<OnRow<Row>>(
        () => (record, index) => {
            const props = {
                className: 'my-tr-class',
            };

            if (index === 2) {
                return {
                    ...props,
                    onClick: () => console.log('mouse click: ', record, index),
                };
            } else {
                return {
                    ...props,
                };
            }
        },
        []
    );
    const onHeaderRow = useMemo<OnHeaderRow<Row>>(
        () => (columns, index) => {
            return {
                onMouseEnter: () => console.log('mouse enter: ', columns, index),
                onMouseLeave: () => console.log('mouse leave: ', columns, index),
            };
        },
        []
    );

    return <Table columns={columns} dataSource={data} onRow={onRow} onHeaderRow={onHeaderRow} />;
}

Zebra Pattern Table

Use OnRow to set a background color for each row to create a zebra stripped table.

标题大小所有者更新日期
Semi Design 设计稿.fig
2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿
2M
郝郝宣
2020-01-17 05:31
设计文档
34KB
ZZoey Edwards
2020-01-26 11:01
Semi D2C 设计稿.fig
2M
姜姜鹏志
2020-02-02 05:13
Semi D2C 分享演示文稿
2M
郝郝宣
2020-01-17 05:31
Semi D2C 设计文档
34KB
ZZoey Edwards
2020-01-26 11:01
Code
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps, type OnRow } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

export default function TableDemo() {
    const columns: ColumnProps<Row>[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 400,
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
        },
        {
            title: '大小',
            dataIndex: 'size',
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
        },
        {
            title: '',
            dataIndex: 'operate',
            render: () => {
                return <IconMore />;
            },
        },
    ];
    const data: Row[] = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
        {
            key: '4',
            name: 'Semi D2C 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '5',
            name: 'Semi D2C 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '6',
            name: 'Semi D2C 设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    const handleRow: OnRow<Row> = (_record, index) => {
        if (index % 2 === 0) {
            return {
                style: {
                    background: 'var(--semi-color-fill-0)',
                },
            };
        } else {
            return {};
        }
    };

    return <Table columns={columns} dataSource={data} onRow={handleRow} pagination={false} />;
}

Custom Header Style

Return a specific style or className through Column.onHeaderCell to customize the header style.
In the example below, the header background color is changed by passing backgroundColor.

标题大小所有者更新日期
Semi Design 设计稿.fig
2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿
2M
郝郝宣
2020-01-17 05:31
设计文档
34KB
ZZoey Edwards
2020-01-26 11:01
Code
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    nameIconSrc: string;
    size: string;
    owner: string;
    updateTime: string;
    avatarBg: 'grey' | 'red' | 'light-blue';
}

export default function TableDemo() {
    const onHeaderCell = () => ({
        style: {
            backgroundColor: 'var(--semi-color-fill-0)',
        },
    });
    const columns: ColumnProps<Row>[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 280,
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
        },
        {
            title: '大小',
            width: 100,
            dataIndex: 'size',
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            width: 200,
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            width: 300,
            dataIndex: 'updateTime',
        },
        {
            title: '',
            dataIndex: 'operate',
            render: () => {
                return <IconMore />;
            },
        },
    ];

    columns.forEach((item) => (item.onHeaderCell = onHeaderCell));
    const data: Row[] = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    return (
        <>
            <Table columns={columns} dataSource={data} pagination={false} />
        </>
    );
}

Custom Cell Hover Style

Table applies the hover style to the whole row by default. To change it, override the styles with CSS.
In the example below, CSS overrides change the hover background from row highlight to cell highlight.

.component-table-demo-cell-hover-custom {
    .semi-table-tbody {
        // Remove the original row hover style
    .semi-table-row:hover {
            .semi-table-row-cell {
                background-color: transparent;
                background-image: none;
                &.semi-table-cell-fixed-left::before {
                    background-color: transparent;
                }
            }
            // Add a style to a single cell
      .semi-table-row-cell:hover {
                background-color: var(--semi-light-green-1);
            }
        }
    }
}
标题大小所有者更新日期
Semi Design 设计稿.fig
2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿
2M
郝郝宣
2020-01-17 05:31
设计文档
34KB
ZZoey Edwards
2020-01-26 11:01
Code
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

export default function TableDemo() {
    const columns: ColumnProps[] = [
        {
            title: '标题',
            dataIndex: 'name',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
        },
        {
            title: '大小',
            width: 100,
            dataIndex: 'size',
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            width: 200,
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            width: 300,
            dataIndex: 'updateTime',
        },
        {
            title: '',
            dataIndex: 'operate',
            render: () => {
                return <IconMore />;
            },
        },
    ];
    const data = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

    return (
        <>
            <Table columns={columns} dataSource={data} className="component-table-demo-cell-hover-custom" pagination={false} />
        </>
    );
}

Column Ellipsis

Use ellipsis to make cells automatically clipped.

所有者大小更新日期
Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworksPengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang2M2020-02-02 05:13
由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣2M2020-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 frameworksPengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang34KB2020-02-02 05:13
由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣34KB2020-02-02 05:13
Code
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, type ColumnProps } from '@fibrous/semi-ui-react';

export default function TableDemo() {
    const columns: ColumnProps[] = [
        {
            title: '标题',
            dataIndex: 'name',
            fixed: true,
            width: 250,
            filters: [
                {
                    text: 'Semi Design 设计稿',
                    value: 'Semi Design 设计稿',
                },
                {
                    text: 'Semi D2C 设计稿',
                    value: 'Semi D2C 设计稿',
                },
            ],
            onFilter: (value, record) => record.name.includes(value),
            sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
            ellipsis: true,
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            width: 200,
            ellipsis: true,
        },
        {
            title: '大小',
            dataIndex: 'size',
            ellipsis: true,
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            width: 200,
            ellipsis: true,
        },
        {
            title: '',
            dataIndex: 'operate',
            fixed: 'right',
            align: 'center',
            width: 100,
            render: () => {
                return <IconMore />;
            },
        },
    ];
    const data = [
        {
            key: '1',
            name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'red',
        },
        {
            key: '3',
            nameIconSrc: '/assets/docs-icon.png',
            name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
            size: '34KB',
            owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'light-blue',
        },
        {
            key: '4',
            name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'light-blue',
        },
    ];

    return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}

Set ellipsis.showTitle to false to hide the default native HTML title. With column.render you can customize the content prompt.

所有者大小更新日期
Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworksPengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang2M2020-02-02 05:13
由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣2M2020-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 frameworksPengzhi Jiang34KB2020-02-02 05:13
由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。郝宣34KB2020-02-02 05:13
Code
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Typography, type ColumnProps } from '@fibrous/semi-ui-react';

export default function TableDemo() {
    const columns: ColumnProps[] = [
        {
            title: '标题',
            dataIndex: 'name',
            fixed: true,
            width: 250,
            filters: [
                {
                    text: 'Semi Design 设计稿',
                    value: 'Semi Design 设计稿',
                },
                {
                    text: 'Semi D2C 设计稿',
                    value: 'Semi D2C 设计稿',
                },
            ],
            onFilter: (value, record) => record.name.includes(value),
            sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
            ellipsis: { showTitle: false },
            render: (text) => <Typography.Text ellipsis={{ showTooltip: true }}>{text}</Typography.Text>,
        },
        {
            title: '所有者',
            dataIndex: 'owner',
            width: 200,
            ellipsis: { showTitle: false },
            render: (text) => <Typography.Text ellipsis={{ showTooltip: true }}>{text}</Typography.Text>,
        },
        {
            title: '大小',
            dataIndex: 'size',
            ellipsis: true,
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            width: 200,
            ellipsis: true,
        },
        {
            title: '',
            dataIndex: 'operate',
            fixed: 'right',
            align: 'center',
            width: 100,
            render: () => {
                return <IconMore />;
            },
        },
    ];
    const data = [
        {
            key: '1',
            name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'red',
        },
        {
            key: '3',
            nameIconSrc: '/assets/docs-icon.png',
            name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
            size: '34KB',
            owner: 'Pengzhi Jiang',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'light-blue',
        },
        {
            key: '4',
            name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            owner: '郝宣',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'light-blue',
        },
    ];

    return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}

Resizable Column

Basic Resizable Column

For some columns with more content, you can choose to turn on the telescopic column function and pull and pull at the head to realize the real-time change of column width.

But you need to pay attention to some parameters:

  • resizable is set to true or an object
  • Any column in columns that requires a telescopic function should specify the widthfield (if not passed, the column does not have a telescopic function and its column width will be automatically adjusted by the browser)
  • column.resize can take effect after resizable is enabled. After setting to false, the column no longer supports scaling.
  • A column that has been dragged keeps its dragged width, while columns that have not been dragged follow the width in columns

When used with fixed columns, you need to specify a column without setting the width

It is not recommended to use it with scroll.x at the same time. scroll.x specifies that the table has a width range, and stretching columns will expand the column width, which may cause the table to be misaligned

标题
所有者
操作列
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig
25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig
35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig
45 KB
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

export default function TableDemo() {
    const columns: ColumnProps<Row>[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 300,
            resize: false,
            render: (text) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
            filters: [
                {
                    text: 'Semi Design 设计稿',
                    value: 'Semi Design 设计稿',
                },
                {
                    text: 'Semi D2C 设计稿',
                    value: 'Semi D2C 设计稿',
                },
            ],
            onFilter: (value, record) => record.name.includes(value),
        },
        {
            title: '大小',
            dataIndex: 'size',
            width: 200,
            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
            render: (text) => `${text} KB`,
        },
        {
            title: '所有者',
            width: 200,
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
            render: (value) => {
                return dateFns.format(new Date(value), 'yyyy-MM-dd');
            },
        },
        {
            title: '操作列',
            dataIndex: 'operate',
            fixed: 'right',
            width: 100,
            resize: false,
            render: () => {
                return <IconMore />;
            },
        },
    ];

    const [data] = useState(() => {
        const _data: Row[] = [];
        for (let i = 0; i < 46; i++) {
            const isSemiDesign = i % 2 === 0;
            const randomNumber = (i * 1000) % 199;
            _data.push({
                key: '' + i,
                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
                owner: isSemiDesign ? '姜鹏志' : '郝宣',
                size: randomNumber,
                updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
                avatarBg: isSemiDesign ? 'grey' : 'red',
            });
        }
        return _data;
    });

    return <Table columns={columns} dataSource={data} resizable bordered />;
}

Advanced Telescopic Columns

resizable can also be an object, including three event methods:

  • onResize
  • onResizeStart
  • onResizeStop

These three callback will be triggered on changing column width, start changing column width and stop changing column width respectively. Developers can choose to modify the column at this time, such as adding a vertical line effect when dragging, as shown below.

标题
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
显示第 1 条-第 5 条,共 46 条
Code
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { addClass, removeClass } from '@fibrous/semi-foundation/utils/classnames';
import { Table, Avatar, type ColumnProps, type TableResizableProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pagination = { pageSize: 5 };

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

export default function TableDemo() {
    const columns: ColumnProps<Row>[] = [
        {
            title: '标题',
            dataIndex: 'name',
            width: 400,
            render: (text) => {
                return (
                    <div>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </div>
                );
            },
            filters: [
                {
                    text: 'Semi Design 设计稿',
                    value: 'Semi Design 设计稿',
                },
                {
                    text: 'Semi D2C 设计稿',
                    value: 'Semi D2C 设计稿',
                },
            ],
            onFilter: (value, record) => record.name.includes(value),
        },
        {
            title: '大小',
            dataIndex: 'size',
            width: 200,
            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
            render: (text) => `${text} KB`,
        },
        {
            title: '所有者',
            width: 200,
            dataIndex: 'owner',
            render: (text, record) => {
                return (
                    <div>
                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                            {typeof text === 'string' && text.slice(0, 1)}
                        </Avatar>
                        {text}
                    </div>
                );
            },
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
            sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
            render: (value) => {
                return dateFns.format(new Date(value), 'yyyy-MM-dd');
            },
        },
    ];

    const [data] = useState(() => {
        const _data: Row[] = [];
        for (let i = 0; i < 46; i++) {
            const isSemiDesign = i % 2 === 0;
            const randomNumber = (i * 1000) % 199;
            _data.push({
                key: '' + i,
                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
                owner: isSemiDesign ? '姜鹏志' : '郝宣',
                size: randomNumber,
                updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
                avatarBg: isSemiDesign ? 'grey' : 'red',
            });
        }
        return _data;
    });

    const resizable: TableResizableProps<Row> = {
        onResizeStart: (curColumn) => {
            const className = addClass(curColumn.className, 'my-resizing');

            return { className };
        },
        onResizeStop: (curColumn) => {
            const className = removeClass(curColumn.className, 'my-resizing');

            return { className };
        },
    };

    return (
        <div id="components-table-demo-resizable-column">
            <Table columns={columns} dataSource={data} resizable={resizable} pagination={pagination} bordered />
        </div>
    );
}

The CSS style definition used in this example:

#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;
}

Drag Sorting

Use dnd-kit with the components API to easily implement drag-and-drop sorting.

标题
所有者
Semi Design 设计稿0.fig
0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-23
Semi Design 设计稿2.fig
10 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-07-23
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿5.fig
25 KB
郝郝宣
2019-07-23
Semi Design 设计稿6.fig
30 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿7.fig
35 KB
郝郝宣
2019-07-23
Semi Design 设计稿8.fig
40 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿9.fig
45 KB
郝郝宣
2019-07-23
显示第 1 条-第 10 条,共 46 条
Code
import { useMemo, useState, type ComponentProps, type CSSProperties } from 'react';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { arrayMove } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/react';
import { isSortable, useSortable } from '@dnd-kit/react/sortable';
import clsx from 'clsx';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnRowReturnObject, type TableProps } from '@fibrous/semi-ui-react';

const getData = () => {
    const data = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

interface SortableRowProps extends ComponentProps<'tr'> {
    index: number;
    'data-row-key': string;
}

const SortableRow = (props: SortableRowProps) => {
    // index comes from the table's onRow; it must not reach the tr
    const { index, ...rowProps } = props;
    const { ref, isDragging } = useSortable({ id: props['data-row-key'], index });
    const style: CSSProperties = {
        ...props.style,
        cursor: 'grabbing',
        ...(isDragging ? { zIndex: 999, position: 'relative' } : {}),
    };
    const rowCls = clsx(props.className, { ['isDragging']: isDragging });

    return <tr {...rowProps} className={rowCls} ref={ref} style={style} />;
};

export default function TableDemo() {
    const pageSize = 10;
    const [dataSource, setData] = useState(getData);
    const [pageNum, setPageNum] = useState(1);
    const pageData = useMemo(() => dataSource.slice((pageNum - 1) * pageSize, pageNum * pageSize), [dataSource, pageNum]);
    const columns = useMemo(
        (): ColumnProps[] => [
            {
                title: '标题',
                dataIndex: 'name',
                width: 400,
                render: (text) => {
                    return (
                        <div>
                            <Avatar size="small" shape="square" src="/assets/placeholder/app.svg" alt="" style={{ marginRight: 12 }} />
                            {text}
                        </div>
                    );
                },
                filters: [
                    {
                        text: 'Semi Design 设计稿',
                        value: 'Semi Design 设计稿',
                    },
                    {
                        text: 'Semi D2C 设计稿',
                        value: 'Semi D2C 设计稿',
                    },
                ],
                onFilter: (value, record) => record.name.includes(value),
            },
            {
                title: '大小',
                dataIndex: 'size',
                sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
                render: (text) => `${text} KB`,
            },
            {
                title: '所有者',
                dataIndex: 'owner',
                render: (text, record) => {
                    return (
                        <div>
                            <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                                {typeof text === 'string' && text.slice(0, 1)}
                            </Avatar>
                            {text}
                        </div>
                    );
                },
            },
            {
                title: '更新日期',
                dataIndex: 'updateTime',
                sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
                render: (value) => {
                    return dateFns.format(new Date(value), 'yyyy-MM-dd');
                },
            },
        ],
        []
    );

    // Sortable indexes are relative to the current page
    const handleDragEnd: ComponentProps<typeof DragDropProvider>['onDragEnd'] = (event) => {
        const { source } = event.operation;
        if (event.canceled || !isSortable(source) || source.initialIndex === source.index) {
            return;
        }
        const offset = (pageNum - 1) * pageSize;
        setData((prev) => arrayMove(prev, offset + source.initialIndex, offset + source.index));
    };

    const handleChange: TableProps['onChange'] = ({ pagination }) => {
        if (pagination?.currentPage) {
            setPageNum(pagination.currentPage);
        }
    };

    return (
        <DragDropProvider
            // Accessibility would add role="button" and a Tab stop to each row; keep table row semantics
            plugins={(defaults) => defaults.filter((plugin) => plugin !== Accessibility)}
            modifiers={[RestrictToVerticalAxis]}
            onDragEnd={handleDragEnd}
        >
            <Table
                components={{
                    body: {
                        row: SortableRow,
                    },
                }}
                rowKey="key"
                columns={columns}
                dataSource={pageData}
                onRow={(_record, index) => {
                    const rowProps: OnRowReturnObject & { index: number } = { index };
                    return rowProps;
                }}
                pagination={{ currentPage: pageNum, pageSize: pageSize, total: dataSource.length }}
                onChange={handleChange}
            />
        </DragDropProvider>
    );
}

Table Grouping

For some tables whose data needs to be displayed in groups, you can pass in groupBy to define the grouping rules, and use renderGroupSection to define the rendering of the grouping table header.

Note: Be sure to provide a "key" for each row of data that is different from other row values, or use the rowKey parameter to specify a property name as the primary key.

标题
所有者
Semi Design 设计稿0.fig
100 KB
姜姜鹏志
2019-10-31
Semi D2C 设计稿19.fig
100 KB
郝郝宣
2019-10-31
Semi Design 设计稿38.fig
100 KB
姜姜鹏志
2019-10-31
Semi D2C 设计稿1.fig
112 KB
郝郝宣
2019-11-12
Semi Design 设计稿20.fig
112 KB
姜姜鹏志
2019-11-12
Semi D2C 设计稿39.fig
112 KB
郝郝宣
2019-11-12
Semi Design 设计稿2.fig
105 KB
姜姜鹏志
2019-11-05
Semi D2C 设计稿21.fig
105 KB
郝郝宣
2019-11-05
Semi Design 设计稿40.fig
105 KB
姜姜鹏志
2019-11-05
Semi D2C 设计稿3.fig
117 KB
郝郝宣
2019-11-17
显示第 1 条-第 10 条,共 46 条
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text) => {
            return (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 46; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = ((i * 1000) % 19) + 100;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

const data = getData();

export default function TableDemo() {
    const rowKey = (record: Row) => `${record.owner.toLowerCase()}-${record.name.toLowerCase()}`;

    return (
        <div style={{ padding: '20px 0px' }}>
            <Table
                dataSource={data}
                rowKey={rowKey}
                groupBy="size"
                columns={columns}
                renderGroupSection={(groupKey) => <strong>根据文件大小分组 {groupKey} KB</strong>}
                onGroupedRow={(group, index) => {
                    return {
                        onClick: () => {
                            console.log(`Grouped row clicked: `, group, index);
                        },
                    };
                }}
                clickGroupedRowToExpand // 点击整行即可展开
                scroll={{ y: 480 }}
            />
        </div>
    );
}

Virtualized Table

Virtualization can be used for scenes that need to render large-scale data. You can enable this feature by configuring the virtualized parameter. have to be aware of is:

  • Must pass scroll.y (number) and style.width (number);
  • Need to pass the height of each line virtualized.itemSize (when not passed, the default row height follows size: 53 for default, 45 for middle and 37 for small, and a group header row is as high as a normal row), can be of the following types:
    • number
    • (index, { sectionRow?: boolean, expandedRow?: boolean }) => number
  • Table grouping virtualization is supported.
  • The virtualized object also takes overscanCount: the number of rows rendered above and below the visible area, 2 by default

Use getVirtualizedListRef to get the ref of the virtual list. ref.current provides scrollTo(offset) and scrollToItem(index, align), where align is auto (default), smart, center, start or end.

The following is an example of rendering 1000 pieces of data.

标题
所有者
Code
import { useRef } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Button, type ColumnProps, type TableGetVirtualizedListRef } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 200,
        fixed: true,
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        width: 150,
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        fixed: 'right',
        width: 150,
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const getData = () => {
    const data: Row[] = [];
    for (let i = 0; i < 1000; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

const data = getData();

export default function TableDemo() {
    const virtualizedListRef = useRef<Parameters<TableGetVirtualizedListRef>[0] | null>(null);
    const scroll = { y: 400, x: 900 };
    const style = { width: 750, margin: '0 auto' };

    return (
        <>
            <Button onClick={() => virtualizedListRef.current?.current.scrollToItem(100)}>Scroll to 100</Button>
            <Table
                pagination={false}
                columns={columns}
                dataSource={data}
                scroll={scroll}
                style={style}
                virtualized
                getVirtualizedListRef={(ref) => {
                    virtualizedListRef.current = ref;
                }}
            />
        </>
    );
}

Infinite Scroll

Based on the virtualization feature, we can achieve infinite scroll loading data by passing in virtualized.onScroll.

标题
所有者
Code
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type TableVirtualizedProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 200,
        fixed: true,
        render: (text) => {
            return <div>{text}</div>;
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        width: 150,
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        fixed: 'right',
        width: 150,
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const pageSize = 20; // 每次加载 20 条

const getPage = (start: number) => {
    const page: Row[] = [];
    for (let i = start; i < start + pageSize; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        page.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return page;
};

export default function TableDemo() {
    const [data, setData] = useState(() => getPage(0));

    const scroll = { y: 600, x: 1000 };
    const style = { width: 750, margin: '0 auto' };

    const loadMore = () => {
        setData([...data, ...getPage(data.length)]);
    };

    const itemSize = 56;
    const virtualized: TableVirtualizedProps = {
        itemSize,
        onScroll: ({ scrollDirection, scrollOffset = 0, scrollUpdateWasRequested }) => {
            if (scrollDirection === 'forward' && scrollOffset >= (data.length - Math.ceil(scroll.y / itemSize) * 1.5) * itemSize && !scrollUpdateWasRequested) {
                loadMore();
            }
        },
    };

    return <Table pagination={false} columns={columns} dataSource={data} scroll={scroll} style={style} virtualized={virtualized} />;
}

Controlled Dynamic Tables

固定表头:
隐藏表头:
显示标题:
显示底部:
固定列:
显示选择列:
显示加载状态:
无数据:
开启排序功能:
开启过滤功能:
开启行展开功能:
展开当前所有行:
显示边框:
开启列伸缩功能:
分页控件:
标题
所有者
暂无数据
显示第 1 条-第 8 条,共 46 条
Code
import { useState, useEffect, useCallback, type ComponentProps, type CSSProperties, type ReactNode } from 'react';
import * as dateFns from 'date-fns';
import { Table, Switch, ButtonGroup, Button, Avatar, type TableChangeInfo, type ColumnProps, type RowSelectionProps, type TableScroll, type TablePaginationProps, type TableProps, type TablePaginationPosition as PaginationPosition } from '@fibrous/semi-ui-react';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: string;
    description?: string;
}

interface PageState extends TablePaginationProps {
    currentPage: number;
    pageSize: number;
    total: number;
}

interface FetchResult {
    dataSource: Row[];
    pagination: PageState;
    sorter: ColumnProps;
    filters: ColumnProps[];
}

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const dataTotalSize = 46;
const wrapStyle: CSSProperties = {
    marginBottom: 15,
    display: 'flex',
    justifyContent: 'space-around',
    flexWrap: 'wrap',
};

const initialColumns: ColumnProps[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text) => {
            return (
                <span>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </span>
            );
        },
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const allData = Array.from({ length: dataTotalSize }, (_v, i): Row => {
    const isSemiDesign = i % 2 === 0;
    const randomNumber = (i * 1000) % 199;
    return {
        key: '' + i,
        name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
        owner: isSemiDesign ? '姜鹏志' : '郝宣',
        size: randomNumber,
        updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
        avatarBg: isSemiDesign ? 'grey' : 'red',
        description: `这是第 ${i} 份设计稿的详细说明。`,
    };
});

const defaultPagination: PageState = {
    currentPage: 1,
    pageSize: 8,
    total: allData.length,
};

const renderExpandedRow = (record: Row) => {
    return {
        children: <p>{record.description}</p>,
        fixed: 'left' as const,
    };
};

const renderFooter = () => <p style={{ margin: 0 }}>This is footer.</p>;

const mergeColumns = (column: ColumnProps, columns: ColumnProps[], keys: (keyof ColumnProps)[] = ['dataIndex']) => {
    const nextColumns = [...columns];
    nextColumns.forEach((curColumn, index) => {
        let isTarget = !!(keys && keys.length);

        for (const key of keys) {
            if (column[key] !== curColumn[key]) {
                isTarget = false;
                break;
            }
        }

        if (isTarget) {
            nextColumns[index] = { ...curColumn, ...column };
        }
    });

    return nextColumns;
};

const filterData = (filters: ColumnProps[], dataSource: Row[]) => {
    let result = [...dataSource];
    filters.forEach((filter) => {
        const filteredValue = filter.filteredValue;
        const dataIndex = filter.dataIndex;
        if (Array.isArray(filteredValue) && filteredValue.length && dataIndex) {
            result = result.filter((data) => filteredValue.filter((value) => String(data[dataIndex as keyof Row]).indexOf(value) > -1).length);
        }
    });

    return result;
};

const sortData = (sortObj: ColumnProps, dataSource: Row[]) => {
    const { sortOrder, dataIndex = '' } = sortObj;
    let sorter = sortObj.sorter;
    let result = dataSource;

    if (sorter && sortOrder && typeof sorter !== 'function') {
        sorter = (a, b) => (a[dataIndex] > b[dataIndex] ? 1 : -1);
    }

    if (typeof sorter === 'function') {
        result = [...result].sort(sorter);

        if (sortOrder === 'descend') {
            result = result.reverse();
        }
    }

    return result;
};

const fetchData = (pagination: PageState, currentPage = 1, sorter: ColumnProps = {}, filters: ColumnProps[] = []) => {
    const nextPagination = { ...pagination, currentPage };
    return new Promise<FetchResult>((resolve) => {
        setTimeout(() => {
            let data = sortData(sorter, allData);
            data = filterData(filters, data);
            const dataSource = data.slice((currentPage - 1) * nextPagination.pageSize, currentPage * nextPagination.pageSize);
            nextPagination.total = data.length;
            resolve({
                dataSource,
                pagination: nextPagination,
                sorter,
                filters,
            });
        }, 1500);
    });
};

const getSorterColumn = (columns: ColumnProps[]) => columns.filter((column) => !!column.sorter)[0];

const getFilterColumns = (columns: ColumnProps[]) => columns.filter((column) => Array.isArray(column.filteredValue) && column.filteredValue.length);

interface TableSwitchProps {
    text: string;
    children?: ReactNode;
    checked?: boolean | number | string;
    onChange?: (checked: boolean) => void;
    style?: CSSProperties;
}

function TableSwitch({ text, children, checked, onChange, style = { display: 'inline-flex', alignItems: 'center', margin: 5 } }: TableSwitchProps) {
    const switchProps: ComponentProps<typeof Switch> = { onChange };

    if (checked != null) {
        switchProps.checked = !!checked;
    }
    return (
        <span style={style}>
            <span>{text}</span>
            {children != null ? children : <Switch size="small" aria-label={text} {...switchProps} />}
        </span>
    );
}

export default function TableDemo() {
    const [loading, setLoading] = useState(true);
    const [columns, setColumns] = useState(initialColumns);
    const [scroll, setScroll] = useState<TableScroll>({});
    const [rowSelection, setRowSelection] = useState<RowSelectionProps<Row>>();
    const [expandable, setExpandable] = useState(false);
    const [expandCellFixed, setExpandCellFixed] = useState(false);
    const [showTitle, setShowTitle] = useState(false);
    const [showFooter, setShowFooter] = useState(false);
    const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
    const [showHeader, setShowHeader] = useState(true);
    const [bordered, setBordered] = useState(false);
    const [resizable, setResizable] = useState(false);
    const [pagination, setPagination] = useState<PageState | false>(defaultPagination);
    const [dataSource, setDataSource] = useState<Row[]>([]);

    const load = useCallback((currentPagination: PageState, currentPage?: number, sorter?: ColumnProps, filters?: ColumnProps[]) => {
        fetchData(currentPagination, currentPage, sorter, filters)
            .then((result) => {
                setColumns((prevColumns) => {
                    let nextColumns = mergeColumns(result.sorter, prevColumns);
                    for (const filterObj of result.filters) {
                        nextColumns = mergeColumns(filterObj, nextColumns);
                    }
                    return nextColumns;
                });
                setLoading(false);
                setPagination(result.pagination);
                setDataSource(result.dataSource);
            })
            .catch((err) => {
                console.error(err);
                setLoading(false);
            });
    }, []);

    const setPage = (page?: number | null, sorter?: ColumnProps | null, filters?: ColumnProps[]) => {
        if (loading) {
            return;
        }
        const currentPage = typeof page === 'number' ? page : (pagination && pagination.currentPage) || 1;

        setLoading(true);
        load(pagination || defaultPagination, currentPage, sorter || getSorterColumn(columns), filters || getFilterColumns(columns));
    };

    useEffect(() => {
        load(defaultPagination, 1, getSorterColumn(initialColumns), []);
    }, [load]);

    const toggleFixHeader = (checked: boolean) => {
        setScroll({ ...scroll, y: checked ? 300 : undefined });
    };

    const toggleFixColumns = (checked: boolean) => {
        const lastIndex = columns.length - 1;
        if (checked) {
            setColumns(
                columns.map((column, index) => {
                    if (index === 0) {
                        return { ...column, fixed: true };
                    }
                    return index === lastIndex ? { ...column, fixed: 'right' } : column;
                })
            );
        } else {
            setColumns(columns.map((column) => ({ ...column, fixed: false })));
        }
        if (rowSelection) {
            setRowSelection({ ...rowSelection, fixed: checked });
        }
        setScroll({ ...scroll, x: checked ? '150%' : undefined });
        setExpandCellFixed(checked);
    };

    const toggleRowSelection = (checked: boolean) => {
        setRowSelection(
            checked
                ? {
                      width: 48,
                      fixed: true,
                      onChange: (selectedRowKeys, selectedRows) => console.log('Selection changed, selectedRowKeys: ', selectedRowKeys, 'selectedRows: ', selectedRows),
                  }
                : undefined
        );
    };

    const toggleShowSorter = (checked: boolean) => {
        setColumns(
            columns.map((column) => ({
                ...column,
                sorter: checked ? initialColumns.find((item) => item.dataIndex === column.dataIndex)?.sorter : undefined,
            }))
        );
    };

    const toggleShowFilter = (checked: boolean) => {
        if (checked) {
            setColumns(
                columns.map((column) =>
                    column.dataIndex === 'name'
                        ? {
                              ...column,
                              filters: [
                                  {
                                      text: '姓名中包含 1',
                                      value: '1',
                                  },
                                  {
                                      text: '姓名中包含 2',
                                      value: '2',
                                  },
                                  {
                                      text: '姓名中包含 3',
                                      value: '3',
                                  },
                              ],
                              filteredValue: [],
                          }
                        : column
                )
            );
        } else {
            setColumns(columns.map((column) => ({ ...column, filters: undefined, filteredValue: undefined })));
            setPage(null, null, []);
        }
    };

    const onChange = (data: TableChangeInfo<Row>) => {
        console.log('Table changed: ', data);
        const { pagination: tablePagination, sorter, filters } = data;
        setPage(tablePagination?.currentPage, sorter, filters);
    };

    const onExpandedRowsChange: TableProps<Row>['onExpandedRowsChange'] = (rows) => {
        console.log('Expanded rows changed to: ', rows);

        // The demo has no grouped rows, so every entry is a data row
        setExpandedRowKeys((Array.isArray(rows) && rows.map((row) => (row as Row).key)) || []);
    };

    const toggleExpandedRowKeys = (checked: boolean) => {
        if (checked) {
            setExpandedRowKeys(dataSource.filter((data) => data.key).map((data) => data.key));
            setExpandable(true);
        } else {
            setExpandedRowKeys([]);
        }
    };

    const toggleResizable = (checked: boolean) => {
        setResizable(!!checked);
        setBordered(!!checked);
    };

    const toggleDataSource = (checked: boolean) => {
        if (checked) {
            setDataSource([]);
        } else {
            setPage();
        }
    };

    const switchPagination = (position: PaginationPosition | false) => {
        setPagination(position ? { ...defaultPagination, ...pagination, position } : false);
    };

    return (
        <div>
            <div style={wrapStyle}>
                <TableSwitch text="固定表头:" checked={scroll && scroll.y} onChange={toggleFixHeader} />
                <TableSwitch text="隐藏表头:" onChange={(checked) => setShowHeader(!checked)} />
                <TableSwitch text="显示标题:" onChange={setShowTitle} />
                <TableSwitch text="显示底部:" onChange={setShowFooter} />
                <TableSwitch text="固定列:" onChange={toggleFixColumns} />
                <TableSwitch text="显示选择列:" onChange={toggleRowSelection} />
                <TableSwitch text="显示加载状态:" onChange={setLoading} checked={loading} />
                <TableSwitch text="无数据:" onChange={toggleDataSource} checked={!dataSource || !dataSource.length} />
                <TableSwitch text="开启排序功能:" onChange={toggleShowSorter} checked={columns.some((column) => column.sorter)} />
                <TableSwitch text="开启过滤功能:" onChange={toggleShowFilter} checked={columns.some((column) => column.filters)} />
                <TableSwitch text="开启行展开功能:" onChange={setExpandable} checked={expandable} />
                <TableSwitch text="展开当前所有行:" onChange={toggleExpandedRowKeys} />
                <TableSwitch text="显示边框:" onChange={setBordered} checked={bordered} />
                <TableSwitch text="开启列伸缩功能:" onChange={toggleResizable} />
                <TableSwitch text="分页控件:">
                    <ButtonGroup>
                        <Button onClick={() => switchPagination('bottom')}>Bottom</Button>
                        <Button onClick={() => switchPagination('top')}>Top</Button>
                        <Button onClick={() => switchPagination('both')}>Both</Button>
                        <Button onClick={() => switchPagination(false)}>None</Button>
                    </ButtonGroup>
                </TableSwitch>
            </div>
            <Table
                onExpandedRowsChange={onExpandedRowsChange}
                title={showTitle ? 'This is title.' : null}
                footer={showFooter ? renderFooter : null}
                showHeader={showHeader}
                bordered={bordered}
                onChange={onChange}
                expandCellFixed={expandCellFixed}
                expandedRowRender={expandable ? renderExpandedRow : undefined}
                expandedRowKeys={expandedRowKeys}
                rowSelection={rowSelection}
                scroll={scroll}
                columns={columns}
                dataSource={dataSource}
                pagination={pagination && { ...pagination, onPageChange: setPage }}
                loading={loading}
                resizable={resizable}
            />
        </div>
    );
}

Fully custom rendering

Generally, you can use Column.render, but you can also passColumn.useFullRender = true to enable full custom rendering mode. At this time, the components such as checkbox button, expand button, indent will be Pass through to the Column.title andColumn.render methods, you can further define the rendering method of the header and cell content.

The input parameters accepted by Column.title are:

{
    filter: ReactNode, // Filter button
    sorter: ReactNode, // Sort button
    selection: ReactNode, // Select button
}

Column.render The fourth input parameter is:

{
    expandIcon: ReactNode, // Expand button
    selection: ReactNode, // Select button
    indentTex: ReactNode, // Indentation
}

The example below renders the checkbox and content into the same cell and header.

Name
所有者
Semi Design 设计稿0.fig0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig45 KB
郝郝宣
2019-09-06
Semi Design 设计稿10.fig50 KB
姜姜鹏志
2019-09-11
Semi D2C 设计稿11.fig55 KB
郝郝宣
2019-09-16
显示第 1 条-第 12 条,共 46 条
Code
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: ({ sorter, filter, selection }) => (
            <span style={{ display: 'inline-flex', alignItems: 'center', paddingLeft: 20 }}>
                {selection}
                <span style={{ marginLeft: 8 }}>Name</span>
                {sorter}
                {filter}
            </span>
        ),
        dataIndex: 'name',
        width: 400,
        filters: [
            {
                text: 'Semi Design 设计稿',
                value: 'Semi Design 设计稿',
            },
            {
                text: 'Semi D2C 设计稿',
                value: 'Semi D2C 设计稿',
            },
        ],
        onFilter: (value, record) => record.name.includes(value),
        useFullRender: true,
        // The fourth render parameter holds the expand button, selection button and text
        render: (text, _record, _index, { expandIcon, selection, indentText }) => {
            return (
                <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
                    {indentText}
                    {expandIcon}
                    {selection}
                    <span style={{ marginLeft: 8 }}>
                        <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                        {text}
                    </span>
                </span>
            );
        },
    },
    {
        title: '大小',
        dataIndex: 'size',
        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
        render: (text) => `${text} KB`,
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record) => {
            return (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value) => {
            return dateFns.format(new Date(value), 'yyyy-MM-dd');
        },
    },
];

const getData = (total: number) => {
    const data: Row[] = [];
    for (let i = 0; i < total; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(() => getData(46));
    const pagination = useMemo(
        () => ({
            pageSize: 12,
        }),
        []
    );

    const rowSelection = useMemo<RowSelectionProps<Row>>(() => {
        return {
            hidden: true,
            fixed: 'left',
        };
    }, []);

    return <Table pagination={pagination} rowSelection={rowSelection} columns={columns} dataSource={dataSource} onChange={(...args) => console.log(...args)} expandedRowRender={(record) => <article>{record.name}</article>} />;
}

Header Merge

Users can use the header merge function to group the header. The header merge can be combined with fixed column, virtualization, data grouping, column scaling and other functions. It also supports JSX or configurable writing.

Combined Header Configuration Writing

基本信息其他信息更多
标题
所有者
Semi Design 设计稿0.fig0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig45 KB
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '基本信息',
        fixed: 'left',
        children: [
            {
                title: '标题',
                dataIndex: 'name',
                width: 300,
                fixed: true,
                render: (text) => {
                    return (
                        <span>
                            <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                            {text}
                        </span>
                    );
                },
                filters: [
                    {
                        text: 'Semi Design 设计稿',
                        value: 'Semi Design 设计稿',
                    },
                    {
                        text: 'Semi D2C 设计稿',
                        value: 'Semi D2C 设计稿',
                    },
                ],
                onFilter: (value, record) => record.name.includes(value),
            },
            {
                title: '大小',
                dataIndex: 'size',
                width: 100,
                fixed: true,
                sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
                render: (text) => `${text} KB`,
            },
        ],
    },
    {
        title: '其他信息',
        children: [
            {
                title: '所有者',
                dataIndex: 'owner',
                render: (text, record) => {
                    return (
                        <div>
                            <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                                {typeof text === 'string' && text.slice(0, 1)}
                            </Avatar>
                            {text}
                        </div>
                    );
                },
            },
            {
                title: '更新日期',
                dataIndex: 'updateTime',
                sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
                render: (value) => {
                    return dateFns.format(new Date(value), 'yyyy-MM-dd');
                },
            },
        ],
    },
    {
        title: '更多',
        fixed: 'right',
        width: 100,
        align: 'center',
        dataIndex: 'operate',
        render: () => {
            return <IconMore />;
        },
    },
];

const getData = (total: number) => {
    const data: Row[] = [];
    for (let i = 0; i < total; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const data = useMemo(() => {
        const _data = getData(46);
        return _data;
    }, []);

    return <Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={data} scroll={{ x: 1000, y: 400 }} onChange={(...args) => console.log(...args)} columns={columns} />;
}

Merge Header JSX Writing

基本信息其他信息更多
标题
所有者
Semi Design 设计稿0.fig0 KB
姜姜鹏志
2019-07-23
Semi D2C 设计稿1.fig5 KB
郝郝宣
2019-07-28
Semi Design 设计稿2.fig10 KB
姜姜鹏志
2019-08-02
Semi D2C 设计稿3.fig15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig20 KB
姜姜鹏志
2019-08-12
Semi D2C 设计稿5.fig25 KB
郝郝宣
2019-08-17
Semi Design 设计稿6.fig30 KB
姜姜鹏志
2019-08-22
Semi D2C 设计稿7.fig35 KB
郝郝宣
2019-08-27
Semi Design 设计稿8.fig40 KB
姜姜鹏志
2019-09-01
Semi D2C 设计稿9.fig45 KB
郝郝宣
2019-09-06
显示第 1 条-第 10 条,共 46 条
Code
import { useMemo, type ReactNode } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const Column = Table.Column;

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const getData = (total: number) => {
    const data: Row[] = [];
    for (let i = 0; i < total; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const data = useMemo(() => {
        const _data = getData(46);
        return _data;
    }, []);

    const nameFilters = [
        {
            text: 'Semi Design 设计稿',
            value: 'Semi Design 设计稿',
        },
        {
            text: 'Semi D2C 设计稿',
            value: 'Semi D2C 设计稿',
        },
    ];

    const renderName = (text: ReactNode) => {
        return (
            <span>
                <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                {text}
            </span>
        );
    };

    const renderOwner = (text: ReactNode, record: Row) => {
        return (
            <div>
                <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                    {typeof text === 'string' && text.slice(0, 1)}
                </Avatar>
                {text}
            </div>
        );
    };

    return (
        <Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={data} scroll={{ x: 1000, y: 400 }} onChange={(...args) => console.log(...args)}>
            <Column title="基本信息" fixed="left">
                <Column<Row> title="标题" dataIndex="name" width={300} fixed render={renderName} filters={nameFilters} onFilter={(value, record) => record.name.includes(value)} />
                <Column<Row> title="大小" dataIndex="size" width={100} fixed render={(text) => `${text} KB`} sorter={(a, b) => (a.size - b.size > 0 ? 1 : -1)} />
            </Column>
            <Column title="其他信息">
                <Column title="所有者" dataIndex="owner" render={renderOwner} />
                <Column<Row> title="更新日期" dataIndex="updateTime" sorter={(a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1)} render={(value) => dateFns.format(new Date(value), 'yyyy-MM-dd')} />
            </Column>
            <Column title="更多" dataIndex="operate" fixed="right" width={100} align="center" render={() => <IconMore />} />
        </Table>
    );
}

colSpan and rowSpan

  • In addition to merging the headers by writing children, you can merge the headers by setting column.colSpan.
  • Table supports row/column merging. When the cell attribute colSpan or rowSpan in render is set to 0, the set table will not be rendered.
type Render = (
    text: string,
    record: Object,
    index: number,
    options?: RenderOptions
) => {
    children: React.ReactNode;
    // merged into the td props; colSpan or rowSpan 0 skips the cell
    props: React.TdHTMLAttributes<HTMLElement>;
};

interface RenderOptions {
    expandIcon?: React.ReactNode;
    isHovering?: boolean;
}
标题大小所有者
Semi Design 设计稿0.fig
Semi D2C 设计稿1.fig
5 KB
郝郝宣
2019-07-28
姜姜鹏志
Semi D2C 设计稿3.fig
15 KB
郝郝宣
2019-08-07
Semi Design 设计稿4.fig
20 KB
姜姜鹏志
2019-08-12
Code
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RenderReturnObject } from '@fibrous/semi-ui-react';

const figmaIconUrl = '/assets/placeholder/app.svg';

interface Row {
    key: string;
    name: string;
    owner: string;
    size: number;
    updateTime: number;
    avatarBg: 'grey' | 'red';
}

const columns: ColumnProps<Row>[] = [
    {
        title: '标题',
        dataIndex: 'name',
        width: 400,
        render: (text, _record, index) => {
            const children = (
                <div>
                    <Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ marginRight: 12 }} />
                    {text}
                </div>
            );
            const renderObject: RenderReturnObject = { children, props: {} };
            if (index === 0) {
                renderObject.props = {
                    colSpan: 4,
                };
            }
            if (index === 1) {
                renderObject.props = {
                    rowSpan: 2,
                };
            }
            if (index === 2) {
                renderObject.props = {
                    rowSpan: 0,
                };
            }
            return renderObject;
        },
    },
    {
        title: '大小',
        dataIndex: 'size',
        render: (text, _record, index) => {
            if (index === 0) {
                return {
                    children: `${text} KB`,
                    props: {
                        colSpan: 0,
                    },
                };
            }
            if (index === 1) {
                return {
                    children: `${text} KB`,
                    props: {
                        rowSpan: 2,
                    },
                };
            }
            if (index === 2) {
                return {
                    children: `${text} KB`,
                    props: {
                        rowSpan: 0,
                    },
                };
            }
            return `${text} KB`;
        },
    },
    {
        title: '所有者',
        dataIndex: 'owner',
        render: (text, record, index) => {
            const children = (
                <div>
                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
                        {typeof text === 'string' && text.slice(0, 1)}
                    </Avatar>
                    {text}
                </div>
            );
            if (index === 0) {
                return {
                    children,
                    props: {
                        colSpan: 0,
                    },
                };
            }
            return children;
        },
    },
    {
        title: '更新日期',
        dataIndex: 'updateTime',
        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
        render: (value, _record, index) => {
            const children = dateFns.format(new Date(value), 'yyyy-MM-dd');
            if (index === 0) {
                return {
                    children,
                    props: {
                        colSpan: 0,
                    },
                };
            }
            if (index === 1) {
                return {
                    children,
                    props: {
                        rowSpan: 2,
                    },
                };
            }
            if (index === 2) {
                return {
                    children,
                    props: {
                        rowSpan: 0,
                    },
                };
            }
            return children;
        },
    },
];

const DAY = 24 * 60 * 60 * 1000;

const getData = (total: number) => {
    const data: Row[] = [];
    for (let i = 0; i < total; i++) {
        const isSemiDesign = i % 2 === 0;
        const randomNumber = (i * 1000) % 199;
        data.push({
            key: '' + i,
            name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
            owner: isSemiDesign ? '姜鹏志' : '郝宣',
            size: randomNumber,
            updateTime: new Date(2019, 6, 23, 12).valueOf() + randomNumber * DAY,
            avatarBg: isSemiDesign ? 'grey' : 'red',
        });
    }
    return data;
};

export default function TableDemo() {
    const [dataSource] = useState(() => getData(5));

    return <Table columns={columns} dataSource={dataSource} pagination={false} />;
}

API Reference

Table

Props

PropertyDescriptionTypeDefault
borderedWhether to show the outer border and the column borders.booleanfalse
childrenTable.Column children, read when columns is not given.ReactNode-
childrenRecordNameThe field of a record that holds its child records in a tree table.string"children"
classNameClass name of the outermost container.string""
clickGroupedRowToExpandClicking a group row expands or collapses the group.boolean-
columnsThe column configuration, see Column. When given, an empty array included, it takes precedence over the Table.Column children.TypeColumnProps<RecordType>[]-
componentsReplaces the elements Table renders, such as table, header.cell and body.row, with a tag name or a component; the component receives the props of the element it replaces.TableComponents-
dataSourceThe data. Give every record a unique key, or name the primary key field with rowKey.RecordType[][]
defaultExpandAllGroupRowsWhether all group rows start expanded. Has no effect on data loaded later.booleanfalse
defaultExpandAllRowsWhether all rows start expanded. Has no effect on data loaded later.booleanfalse
defaultExpandedRowKeysThe keys of the rows that start expanded. Has no effect on data loaded later.(string | number)[][]
directionThe text direction. Defaults to the direction of ConfigProvider and can be set here per table.Direction-
emptyThe content shown when there is no data. Defaults to the empty text of the locale.ReactNode-
expandAllGroupRowsWhether all group rows are expanded.booleanfalse
expandAllRowsWhether all rows are expanded.booleanfalse
expandCellFixedWhether the column of the expand button is fixed, with the same values as the fixed field of a column.Fixedfalse
expandedRowKeysThe keys of the expanded rows. When given, row expansion is controlled.(string | number)[]-
expandedRowRenderThe extra row rendered under a record. Returns an element, or an object { children, fixed } that sets how the expanded row is fixed. Give every record a unique key, or name the primary key field with rowKey.TypeExpandedRowRender<RecordType>-
expandIconfalse hides the expand buttons. An element or a function replaces the icon; the function receives expanded.ExpandIcon-
expandRowByClickWhether clicking a row expands an expandable row.booleanfalse
footerThe table footer: an element, or a function that receives the data of the current page and returns an element.TypeFooter<RecordType>-
getVirtualizedListRefCalled with the ref of the list once a virtualized table creates it; its current provides scrollTo and scrollToItem. Only effective with virtualized.GetVirtualizedListRef-
groupByThe grouping rule: a record field name, or a function that returns a string or a number.TypeGroupBy<RecordType>-
headerStyleInline style of every header cell, fixed headers included.React.CSSProperties-
hideExpandedColumnIn an expandable table the expand button shares the first cell by default; set false to render it as its own column.booleantrue
idId of the outermost container.string-
indentSizeThe indent of each tree level, in pixels.number20
keepDOMWhether collapsing a row keeps its DOM.boolean-
loadingWhether the table shows its loading state.booleanfalse
onChangeCalled when pagination, sorting or filtering changes. changeInfo holds pagination, filters, sorter and extra, whose extra.changeType is sorter, filter or pagination.TypeOnChange<RecordType>-
onExpandCalled when the expand icon of a row is clicked, with whether the row is now expanded, the record and the mouse event.TypeOnExpand<RecordType>-
onExpandedRowsChangeCalled when the expanded rows change, with the records of the expanded rows.TypeOnExpandedRowsChange<RecordType>-
onGroupedRowLike onRow, for group rows: the returned object is merged into the tr of the group row. index is the group's ordinal, or its position among the virtual list's rows when virtualized is on.TypeOnGroupedRow<RecordType>-
onHeaderRowSets the attributes and events of a header row. The returned object is merged into the tr of the header row, without style; use headerStyle for it.TypeOnHeaderRow<RecordType>-
onRowSets the attributes and events of a data row; the third argument is the row state, with disabled and selected. The returned object is merged into the tr of the row.TypeOnRow<RecordType>-
onRowContextMenuCalled when a data row receives the context menu, before the onContextMenu returned by onRow; both run.Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
onRowDoubleClickCalled when a data row is double-clicked, before the onDoubleClick returned by onRow; both run.Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
onRowMouseEnterCalled when the mouse enters a data row, before the onMouseEnter returned by onRow; both run.Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
onRowMouseLeaveCalled when the mouse leaves a data row, before the onMouseLeave returned by onRow; both run.Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
paginationThe pagination configuration. Pass false to turn pagination off.TablePaginationtrue
prefixClsThe prefix of the class names.string"semi-table"
refThe table handle, which provides the getCurrentPageData method below.TypeReact.Ref<TableHandle<RecordType>>-
renderGroupSectionRenders the content of a group row. An element is the content of the single cell of the group row; an object { children, ...column } gives the content in children and the other fields as the configuration of that column.RenderGroupSection-
renderPaginationRenders a custom pager from the pagination configuration, and returns an element.RenderPagination-
resizableWhether columns can be resized; a resizable column needs a width. An object configures onResize, onResizeStart, onResizeStop and handlerClassName.TypeResizable<RecordType>-
rowExpandableTells whether a row can be expanded. A row that returns false has no expand button.TypeRowExpandable<RecordType>-
rowKeyThe row key: a record field name, or a function that returns a string or a number.TypeRowKey<RecordType>"key"
rowSelectionThe row selection configuration. Pass true for the defaults, see rowSelection.TypeRowSelection<RecordType>null
rowSpanHoverHovering a cell with rowSpan highlights every row the cell spans.boolean-
scrollThe scroll area: x is the width and y the height, see scroll.Scroll-
showHeaderWhether to show the header.booleantrue
sizeThe table size, which sets the padding of the cells.Size"default"
stickySticks the header to the top of the page. With an object, top is the distance from the top of the scroll container.Sticky-
styleStyle of the outermost container.React.CSSProperties-
titleThe table title: an element, or a function that receives the data of the current page and returns an element.TypeTitle<RecordType>-
virtualizedTurns on virtualization, which needs scroll.y as a number. An object sets the row height itemSize, the scroll callback onScroll and the extra rows overscanCount.Virtualized-

Methods

MethodDescriptionType
getCurrentPageDataReturns the data of the current page, deep cloned, as { dataSource, groups }; groups is a Map from each group key to the set of row keys of that group.Type() => Pick<BasePageData<RecordType>, 'dataSource' | 'groups'>

aria-label, aria-labelledby and aria-describedby are placed on the element with role="table"; other native attributes, such as data-* and onClick, are placed on the outermost div.

Fields of virtualized when it is an object:

FieldInstructionsTypeDefault
itemSizeRow height. When not passed, the default follows size: 53 for default, 45 for middle and 37 for small, and a group header row is as high as a normal rownumber | ((index: number, row: { sectionRow?: boolean, expandedRow?: boolean }) => number)53
onScrollVirtualization scroll callback method, called only when the vertical scroll offset changes(object: { scrollDirection?: 'forward' | 'backward', scrollOffset?: number, scrollUpdateWasRequested?: boolean }) => void-
overscanCountRows rendered above and below the visible areanumber2

headerStyle Example

headerStyle will be applied to all header <th> elements, including fixed columns.

import React from 'react';
import { Table } from '@fibrous/semi-ui-react';

const columns = [
    {
        title: 'Name',
        dataIndex: 'name',
        width: 180,
        fixed: 'left',
    },
    {
        title: 'Age',
        dataIndex: 'age',
        width: 120,
    },
    {
        title: 'Address',
        dataIndex: 'address',
        width: 280,
    },
    {
        title: 'Company',
        dataIndex: 'company',
        width: 280,
    },
];

const dataSource = Array.from({ length: 6 }).map((_, i) => ({
    key: i,
    name: `Edward ${i}`,
    age: 20 + i,
    address: 'No. 1, Lake Park',
    company: 'Semi Design',
}));

export default function Demo() {
    return <Table columns={columns} dataSource={dataSource} scroll={{ x: 900 }} headerStyle={{ backgroundColor: '#F5F6F7', fontWeight: 600 }} />;
}

Some of the type definitions used above:

// PaginationProps is the props supported by the Pagination component
interface TablePaginationProps extends PaginationProps {
    position?: PaginationPosition;
    formatPageText?: FormatPageText;
}

type VirtualizedItemSizeFn = (index?: number, row?: { sectionRow?: boolean; expandedRow?: boolean }) => number;
type VirtualizedOnScrollArgs = {
    scrollDirection?: 'forward' | 'backward';
    scrollOffset?: number;
    scrollUpdateWasRequested?: boolean;
};
type VirtualizedOnScroll = (object: VirtualizedOnScrollArgs) => void;

type Virtualized =
    | boolean
    | {
          itemSize?: number | VirtualizedItemSizeFn;
          onScroll?: VirtualizedOnScroll;
      };

RecordType is a generic parameter of Table and Column, and the default is object type. You can use RecordType like this:

import { ColumnProps } from 'table/interface';

interface Record {
    title?: string;
    dataIndex?: string;
    width?: number;
    render?: Function;
    key?: string;
    name?: string;
    age?: number;
    address?: string;
}

const columns: ColumnProps<Record>[] = [
    {
        title: 'Name',
        dataIndex: 'name',
        width: 200,
    },
    // ...
];

const data: Record[] = [
    {
        key: '1',
        name: 'John Brown',
        age: 32,
        address: 'New York No. 1 Lake Park, New York No. 1 Lake Park',
    },
    // ...
];

function App() {
    return <Table<Record> columns={columns} dataSource={data} />;
}

onRow/onHeaderRow Usage

Also in column.onCell column.onHeaderCell Properties or events supported by td / th can also be returned.

The third parameter rowStatus of onRow can get the current row's status information, including disabled and selected properties. This is useful when you need to execute different logic based on the row's selected or disabled state, for example, determining whether to allow selection when clicking a row.

import React from 'react';
import { Table } from '@fibrous/semi-ui-react';

() => (
    <Table
        onRow={(record, index, rowStatus) => {
            return {
                onClick: (event) => {
                    // rowStatus.disabled indicates whether the current row is disabled
                    // rowStatus.selected indicates whether the current row is selected
                    if (rowStatus?.disabled) {
                        console.log('This row is disabled');
                        return;
                    }
                    console.log('Clicked row', index, rowStatus);
                },
                onMouseEnter: (event) => {},
                onMouseLeave: (event) => {},
                className: '',
                // ...
                // Other attributes or events that can be applied to tr
            };
        }}
        onHeaderRow={(columns, index) => {
            return {
                onClick: (event) => {},
                onMouseEnter: (event) => {},
                onMouseLeave: (event) => {},
                className: '',
                // ...
                // Other attributes or events that can be applied to th
            };
        }}
    />
);

Column

A column accepts only the properties below, and a misspelled property name is a type error. To carry custom fields on a column, extend the type with an intersection, such as ColumnProps<RecordType> & { tooltip: string }.

Props

PropertyDescriptionTypeDefault
alignThe alignment of the cell content: left, right or center. Swapped in RTL. left when not set.Align-
childrenChild columns of a merged header.TypeArray<ColumnProps<RecordType>>-
classNameClass name of the column.string-
colSpanThe number of columns the header cell spans; 0 hides the header cell.number-
dataIndexThe field of a record that the column shows. A unique dataIndex is required when sorter or onFilter is used.string-
defaultFilteredValueThe default filter values, an array of the filtered values, used as the initial filter state when filteredValue is not given.any[]-
defaultSortOrderThe default sort order: ascend, descend or false, used as the initial sort state when sortOrder is not given.SortOrder-
ellipsisTruncates text that overflows the cell; the table layout then switches to fixed. With an object, showTitle set to false hides the native HTML title hint, and render can give a custom tip.BaseEllipsis-
filterChildrenRecordWhether filtering also filters the child records of a tree table. When on, a parent record is kept when one of its children matches, even if the parent does not.boolean-
filterConfirmModeThe filter confirm mode. immediate applies a filter option as soon as it is clicked; confirm applies the options after the confirm button is clicked, and the dropdown then shows confirm and reset buttons. Defaults to immediate.FilterConfirmMode-
filterDropdownAn element that replaces the default filter menu; the interactions inside it are written by the caller.ColumnFilterProps['filterDropdown']-
filterDropdownPropsProps passed to Dropdown, see Dropdown API.ColumnFilterProps['filterDropdownProps']-
filterDropdownVisibleControls the visible of the filter Dropdown, see Dropdown API.boolean-
filteredValueThe controlled filter values, an array of the filtered values. When given, the filter state is controlled by the caller.any[]-
filterIconThe filter icon: an element, or a function that receives filtered and returns a node.FilterIcon-
filterMultipleWhether several filter options can be chosen; multiple selection is allowed when not set.boolean-
filtersThe filter menu options of the header. Not required with renderFilterDropdown.Filter[]-
fixedWhether the column is fixed: true (same as left), left or right. Swapped in RTL.Fixed-
keyIdentifies the column for selection, sorting, filtering and width; it can be omitted when a unique dataIndex is set.string | number-
onCellSets the attributes and events of the data cells. It receives the record and the row index, and the returned object is merged into the td.TypeOnCell<RecordType>-
onFilterThe local filter function. It receives the filter value and a record, and keeps the record when it returns true. A unique dataIndex is required with it.TypeOnFilter<RecordType>-
onFilterDropdownVisibleChangeCalled when the visibility of the filter dropdown changes.OnFilterDropdownVisibleChange-
onHeaderCellSets the attributes and events of the header cell. It receives the column, the column index and the header row index, and the returned object is merged into the th.TypeOnHeaderCell<RecordType>-
renderRenders the cell content from (text, record, index, options). Returns a node, or an object { children, props } whose props are merged into the cell (a cell with colSpan 0 is not rendered). options holds expandIcon, selection, indentText and isHovering.TypeColumnRender<RecordType>-
renderFilterDropdownRenders the content of the filter dropdown. The argument holds the temporary filter values, their setter, and the confirm, clear, close and reset methods, see Custom Filter Rendering.ColumnFilterProps['renderFilterDropdown']-
renderFilterDropdownItemRenders each item of the filter menu. The argument holds text, value, checked, onChange, level, filteredValue and filterMultiple.RenderFilterDropdownItem-
resizeWith resizable on the table, false leaves the column without a resize handle.boolean-
shouldCellUpdateDecides whether a cell needs an update; the cell renders when it returns true. By default the cell props are deep compared. Set it when the record is complex, to take over this decision.Type(props: TableCellProps, prevProps: TableCellProps) => boolean-
showSortTipWhether to show the sort tip; not shown when not set. Has no effect when sortOrder is given.boolean-
sortChildrenRecordWhether sorting also sorts the child records of a tree table.boolean-
sorterThe sort function. It receives two records and the current sort order, and returns a negative number, zero or a positive number, as the compare function of Array.sort does. Set true for sorting on the server. A local sorter needs a unique dataIndex, and a unique key on every record.TypeSorter<RecordType>-
sortIconRenders the sort button from sortOrder. The returned node replaces the whole sort button, ascending and descending included, and must highlight the current sortOrder.SortIcon-
sortOrderThe controlled sort order: ascend, descend or false. When given, the sort state is controlled by the caller.SortOrder-
titleThe header content: a node, or a function that receives { filter, sorter, selection }. Without useFullRender the function is called with an empty object and its result is combined with the sort and filter buttons; with it, the result of the function is the whole header.ColumnTitle-
useFullRenderWhether the column renders in full, which lets title and render read the sorter, filter, selection and expandIcon nodes. It costs some performance.boolean-
widthThe column width; a number is in pixels. Required with resizable.string | number-

Some of the type definitions used above:

// RecordType is a generic parameter of Table and Column, the default is object type

type Filter = {
    value: any;
    text: React.ReactNode;
    children?: Filter[];
};

rowSelection

ParametersInstructionsTypeDefault
classNameStyle name listedstring
checkRelationParent-child node selection association mode. When set to 'related', selecting a parent node automatically selects all child nodes, and selecting a child node affects the parent node's state (checked/half-checked/unchecked). Default is 'unRelated', meaning parent and child node selections are independent.'related' | 'unRelated''unRelated'
clickRowWhether to enable click row to select. When enabled, clicking anywhere on the row will trigger selection/deselection, including fixed columns. Disabled rows (via getCheckboxProps) cannot be selected by clicking; when disabled is true, clicking a row does not select it either.booleanfalse
defaultSelectedRowKeysThe keys of the rows selected initially; read only on mount and ignored when selectedRowKeys is set(string | number)[]
disabledDisabled Checkbox in Table header or not; when true, clickRow selection does not take effect eitherbooleanfalse
fixedSecure the selection box column to the left.booleanfalse
getCheckboxPropsDefault property configuration for the selection box(record: RecordType) => object
hiddenHide selection column or notbooleanfalse
selectedRowKeysSpecifies the key array of the selected item, which needs to work with onChangestring []
shouldCellUpdateSelf control whether cell should be updated(props: TableCellProps, prevProps: TableCellProps) => boolean
renderCellCustom rendering checkbox({ selected: boolean, record: RecordType, originNode: JSX.Element, inHeader: boolean, disabled: boolean, indeterminate: boolean, index?: number, selectRow?: (selected: boolean, e: Event) => void, selectAll?: (selected: boolean, e: Event) => void }) => ReactNode
widthCustom list selection box widthstring | number
onChangeA callback in the event of a change in the selected item. The first parameter will save the row keys selected last time, even if you do paging control or update the dataSource FAQ(selectedRowKeys: number[]|string[], selectedRows: RecordType[]) => void
onHeaderCellSet the head cell property; index is the index of the header row(column: Column, columnIndex: number, index: number) => object
onSelectCallback when the user manually selects a row. nativeEvent is the selection box's change event, the mouse event when clickRow selects on a row click, or the event passed to renderCell's selectRow(record: RecordType, selected: boolean, selectedRows: RecordType[], nativeEvent: CheckboxEvent | React.MouseEvent | Event) => void
onSelectAllThe user manually clicks the callback of the header selection box, and all selectable rows in the current filter result (disabled rows excluded) will be selected/unselected. changedRows contains only the rows whose selection state changed. e is the selection box's change event, or the event passed to renderCell's selectAll(selected: boolean, selectedRows: RecordType[], changedRows: RecordType[], e: CheckboxEvent | Event) => void

scroll

ParametersInstructionsTypeDefault
scrollToFirstRowOnChangeWhether to automatically scroll to the top of the table after paging, sorting, and filtering changes. When scroll.y is set, it scrolls the table body to the top; when scroll.y is not set, it scrolls the page to the table header positionbooleanfalse
xSet the width of the horizontal scroll area, which can be pixel value, percentage, or 'max-content'string|number
ySet the height of the vertical scroll area, which can be a pixel valuenumber

pagination

Page-turning component configuration. Pagination suggests not to use literal value.

Note: After pagination.onChange is set, Table onChange no longer responds to pagination changes.

ParametersInstructionsTypeDefault
currentPageCurrent page numbernumber-
defaultCurrentPageDefault current page numbernumber1
formatPageTextPage-turning area copywriting custom formatting, pass false to close copywriting display; This item affects the copy display on the left of the page turning area of the form. It is different from the showTotal parameter of thePagination component.boolean| ({ currentStart: number, currentEnd: number, total: number }) => string|ReactNodetrue
pageSizeNumber of entries per pagenumber10
positionLocation'bottom '|'top '|'both''bottom'
totalTotal number of entriesnumber0
preventPageChangeOnPageSizeChangeWhether to prevent automatic adjustment of currentPage when pageSize changes. By default, when pageSize changes, the component automatically calculates the new currentPage to maintain the current data position. When set to true, the user controls the page changebooleanfalse

For other configurations, see Pagination

Resizable

The parameters of the resizable object type, which mainly include event methods when the table column is scaled. These event methods can return an object that merges with the final column.

ParametersInstructionsTypeDefault
handlerClassNameThe class name added to the column while it is being resizedstring'resizing'
onResizeTriggers when the table column changes its width(column: Column) => Column | void
onResizeStartTriggers when the table column starts to change the width.(column: Column) => Column | void
onResizeStopTriggers when the table column stops changing the width(column: Column) => Column | void

Methods

Some internal methods provided by Table can be accessed through ref:

import React, { useRef, useEffect } from 'react';
import { Table } from '@fibrous/semi-ui-react';

function Demo() {
    const ref = useRef();

    useEffect(() => {
        ref.current.getCurrentPageData(); // => { dataSource: [/*...*/], groups: /*...*/ }
    }, []);

    return <Table columns={[/*...*/]} dataSource={[/*...*/]} ref={ref} />;
}

The methods on the handle are listed in the Methods table of the Table API above.

Accessibility

ARIA

Table follows the WAI-ARIA Table pattern: it is a static table, arrow keys do not move between cells, and every interactive part (sort, filter, expand, select) is a native control reachable with Tab.

  • The table's role is table, rows are row, header cells are columnheader and cells are cell. A tree table is a table too and does not expose row levels
  • The sort button is a native button in the header cell, named by the column title. The sort direction is aria-sort on the header cell (ascending or descending); unsorted columns do not carry it. The showSortTip tip shows when the button is focused
  • The expand button is a native button named "Expand this row", with aria-expanded telling whether the row is expanded
  • The row selection checkboxes have accessible names such as "Select this row" and "Select all rows"
  • With a fixed header (scroll.y, sticky) or virtualization, the header and the body render as two table elements; their common container has the table role and the inner tables have role none, so cells stay associated with their column headers
  • In a virtualized table aria-rowcount is the header row count plus the total data row count, and data rows' aria-rowindex continues after the header rows, so screen readers announce rows that are not in the DOM
  • The default column filter icon is a native button: Tab focuses it, Enter, Space or Down Arrow opens the filter menu, and it carries aria-haspopup="menu" and aria-expanded. With a custom filterIcon, keeping it focusable is up to you

Known limitations:

  • In a sortable column the title renders inside the sort button, so do not put buttons, links or other interactive content in it

RTL/LTR

Content Guidelines

  • Table title
    • The title of the table should clearly make the user perceive the purpose of the table;
    • Add descriptions to complex tables to provide users with more contextual information about the table;
    • use sentence case;
  • Column headers
    • Keep column headings concise, it is recommended to use 1-2 words as column headings;
    • When the column header is long, it is recommended to display it in 2 lines, and the remaining text is abbreviated and displayed completely in the Tooltip;
    • Adopt the capitalization rules of Sentence case;
    • Use sentence case for column headings;
  • Table operation area

Design Tokens

These are the component's internal Sass variables and cannot be overridden. To customize styles, override the --semi-* CSS variables (see the Design Token page) or write unlayered CSS rules.
Sass variableValueUsage
$transition_duration-table_body-bgvar(--semi-transition_duration-none)表格-背景颜色-动画持续时间
$transition_function-table_body-bgvar(--semi-transition_function-easeOut)表格-背景颜色-过渡曲线
$transition_delay-table_body-bg0ms表格-背景颜色-延迟时间
$transition_duration-table_row-head-bgvar(--semi-transition_duration-none)表格-行头-背景颜色-动画持续时间
$transition_function-table_row-head-bglinear表格-行头-背景颜色-过渡曲线
$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-paddingX0表格标题水平内边距
$spacing-table_footer-padding$spacing-base表格 footer 内边距
$spacing-table_expand_row-paddingLeft16px表格展开行左侧内边距
$spacing-table_expand_row-paddingRight16px表格展开行右侧内边距
$spacing-table_resizable-offset-y4px可拖动拉伸操作垂直偏移量
$spacing-table_resizable-bottom4px可拖动拉伸操作底部距离
$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-paddingX16px表格选择状态行(非复选框列)单元格水平内边距
$spacing-table_tbody_rowSelection_rowCell_notSelection-paddingY10px表格选择状态行(非复选框列)垂直内边距
$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-marginRight8px行展开按钮右侧外边距
$spacing-table_panel_operation-paddingX$spacing-base表格操作栏水平内边距
$spacing-table_panel_operation-paddingY$spacing-tight表格操作栏垂直内边距
$width-table_base_border1px表格单元格分割线宽度
$width-table_header_border1px表格表头分割线宽度
$width-table_resizer_border2px表格拉伸列标示线宽度
$width-table_column_selection48px表格选择列及展开列宽度
$width-table_column_sorter-icon16px表格排序按钮宽度
$height-table_column_sorter-icon16px表格排序按钮高度
$width-table_column_filter-icon16px表格过滤按钮宽度
$height-table_column_filter-icon16px表格过滤按钮高度
$width-table_cell_fixed_left_last1px表格左侧固定列右侧分割线宽度
$width-table_cell_fixed_right_first1px表格右侧固定列左侧分割线宽度
$width-table_react_resizable_handle9px表格伸缩列调节热区宽度
$height-table_pagination_outer_min60px表格分页器高度
$height-table_column_filter_dropdown290px表格过滤筛选列表最大高度
$color-table_panel-bg-defaultvar(--semi-color-primary)操作区域样式默认背景颜色
$color-table_panel-text-defaultvar(--semi-color-primary-light-active)表格操作栏选中项文本颜色
$color-table-bg-defaultvar(--semi-color-bg-1)表格固定列及吸顶表头背景颜色
$color-table-text-defaultvar(--semi-color-text-0)单元格默认文字颜色
$color-table-border-defaultvar(--semi-color-border)表格描边颜色
$color-table_shadow-bg-defaultrgb(from var(--semi-black) r g b / 0.08)表格滚动后阴影颜色
$color-table_shadow-bg-darkrgb(from var(--semi-black) r g b / 0.4)表格滚动后阴影颜色 - 暗色模式
$color-table_shadow-border-defaultvar(--semi-color-border)表格拟阴影 描边颜色
$color-table_th-bg-defaultvar(--semi-color-bg-1)表头背景底色,须不透明以遮住滚动到固定列和吸顶表头下的内容
$color-table_th-fill-defaultvar(--semi-color-fill-0)表头背景叠加的填充色
$color-table_th-border-defaultvar(--semi-color-border)表头底部分割线颜色
$color-table_th-text-defaultvar(--semi-color-text-2)表头文字颜色
$color-table_th-clickSort-bg-hovervar(--semi-color-fill-1)点击表头触发排序背景色 - 悬浮
$color-table_pl-bg-defaulttransparent
$color-table_body-bg-defaultvar(--semi-color-bg-1)表格行背景颜色 - 默认态
$color-table_body-bg-hovervar(--semi-color-fill-0)表格行背景颜色 - 悬浮态
$color-table_footer-bg-defaultvar(--semi-color-fill-0)表格 footer 背景颜色 - 默认
$color-table_row_expanded-bg-defaultvar(--semi-color-fill-0)表格展开行背景颜色 - 默认
$color-table_expanded_icon-defaultvar(--semi-color-text-2)表格展开行图标颜色 - 默认
$color-table_expanded-bg-defaulttransparent表格展开行图标容器背景颜色 - 默认
$color-table_filter_on-text-defaultvar(--semi-color-primary)表格过滤按钮颜色 - 激活态
$color-table_filter-text-defaultvar(--semi-color-text-2)表格过滤按钮颜色 - 默认态
$color-table_sorter_on-text-defaultvar(--semi-color-primary)表格排序按钮颜色 - 激活态
$color-table_sorter-text-defaultvar(--semi-color-text-2)表格排序按钮颜色 - 默认态
$color-table_sorter-text-hovervar(--semi-color-text-2)表格排序按钮颜色 - 悬浮态
$color-table_page-text-defaultvar(--semi-color-text-2)表格翻页器文本颜色
$color-table_resizer-bg-defaultvar(--semi-color-primary)表格拉伸标示线颜色
$color-table_selection-bg-defaultvar(--semi-grey-0)表格分组背景色
$color-table_placeholder-text-defaultvar(--semi-color-text-2)表格空数据文本颜色
$color-table_cell-bg-hovervar(--semi-color-bg-0)让表格在 hover 时正确显示 $color-table_body-bg-hover 颜色,如无必要不要修改
$font-table_base-fontSize14px表格默认文本字号
$border-table_base-borderStylesolid表格描边样式
$width-table_fixed_shadow6px表格固定列滚动阴影宽度
$z-table_fixed_column$z-table-fixedfixed列的zIndex值

FAQ

  • Clicking the row selection button on the second page will jump to the first page?

    After the Table's dataSource is updated, the page number will be reset to the initial state. Please check if the data source changed when the component was rendered.

    function App() {
        const [dataSource, setDataSource] = useState([]);
    
        useEffect(() => {
            // ✅ Correct
            const getData = () => {
                // fetch data
                const newData = fetch(/**/);
                // set data
                setDataSource(dataSource);
            };
    
            getData();
        }, []);
    
        // ❌ Error
        const data = [];
    
        return <Table dataSource={data} columns={[/*...*/]} />;
    }
  • The number of filtered data is wrong?

    Please check that your filter columns and data sources are configured correctly.

    The filter column needs to set an independent dataIndex, and the dataSource needs to set an independent key. Please refer to the dataSource API. Otherwise the filtering function will not work properly.

  • Why is the table data not updated?

    At present, all parameters of the table component are shallow comparison. That is to say, if the parameter value type is an array or object, you need to manually change its reference to trigger the update. Similarly, if you don't want to trigger additional updates, try not to use literal values when passing parameters directly or define reference parameter values in the render process:

    // ...render() {
        <Table dataSource={[/*...*/]} columns={[/*...*/]} />
    // }

    The above writing method will trigger the update of data in the table every time render (the current selected row will be cleared and the row key array will be expanded, etc.). In order to improve performance and avoid some exceptions, please define some reference type parameters outside the render method as far as possible (if hooks are used, please use useMemo or useState for storage).

  • Why can't my form line be selected and expanded?

    Please specify a rowKey or set a different "key" attribute for each item of the dataSource. All rows related operations in the table need to be used.

  • How to implement custom sorting or pass parameters to the server for sorting when clicking the sort button?

    The input parameters of the onChange method include pagination, filters, and sorter. Users can customize the sorting of the dataSource according to the sorter.

  • How to add className to a row?

    Use onRow or onHeaderRow.

  • How to style the table cell?

    It can be controlled by column.onHeaderCell and column.onCell.

  • Why cache the previously selected keys for the first parameter of rowSelection onChange?

    This is for the scenario where the selected row keys are lost when data is selected on the first page during paging, and then the data is selected on the second page. If you don't want to use the cached keys, you can filter it from the current dataSource or use the second parameter of rowSelection onChange.

  • Does it support single row selection?

    Table currently does not support single-row selection function, and users can implement single selection in a custom way.