展示类

表格

表格用于呈现结构化的数据内容,通常会伴随提供对数据进行操作(排序、搜索、分页……)的能力。

如何使用

往 Table 传入表头 columns 和数据 dataSource 进行渲染。

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

function App() {
    const columns = [
        {
            title: '标题',
            dataIndex: 'name',
        },
        {
            title: '大小',
            dataIndex: 'size',
        },
        {
            title: '所有者',
            dataIndex: 'owner',
        },
        {
            title: '更新日期',
            dataIndex: 'updateTime',
        },
    ];
    const data = [
        {
            key: '1',
            name: 'Semi Design 设计稿.fig',
            nameIconSrc: '/assets/placeholder/app.svg',
            size: '2M',
            owner: '姜鹏志',
            status: 'success',
            updateTime: '2020-02-02 05:13',
            avatarBg: 'grey',
        },
        {
            key: '2',
            name: 'Semi Design 分享演示文稿',
            nameIconSrc: '/assets/docs-icon.png',
            size: '2M',
            owner: '郝宣',
            status: 'pending',
            updateTime: '2020-01-17 05:31',
            avatarBg: 'red',
        },
        {
            key: '3',
            name: '设计文档',
            nameIconSrc: '/assets/docs-icon.png',
            size: '34KB',
            status: 'wait',
            owner: 'Zoey Edwards',
            updateTime: '2020-01-26 11:01',
            avatarBg: 'light-blue',
        },
    ];

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

代码演示

基本表格

对于表格,最基本的两个参数为 dataSource 和 columns,前者为数据项,后者为每列的配置,二者皆为数组类型。

标题大小交付状态所有者更新日期
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
代码
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

JSX 写法

你也可以使用 JSX 语法定义 columns,注意 Table 仅支持 columns 的 JSX 语法定义。你不能够使用任何组件包裹 Table.Column 组件。

标题大小所有者更新时间
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
代码
import type { ComponentProps } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar } from '@fibrous/semi-ui-react';

const { Column } = Table;

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

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

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

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

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

行选择操作

往 Table 传入 rowSelection 即可打开此功能。

  • 点击表头的选择框,会选择 dataSource 里所有不是 disabled 状态的行。选择所有行回调函数为 onSelectAll;
  • 点击行的选择框会选中当前行。它的回调函数为 onSelect;
标题大小交付状态所有者更新日期
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 条
代码
import { useMemo } from 'react';
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

自定义渲染

用户可以使用 Column.render 来自定义某一列单元格的渲染,该功能适用于需要渲染较为复杂的单元格内容时。

render 函数的第四个参数 options 是一个对象,包含以下属性:

  • expandIcon: 展开图标(当使用树形数据或可展开行时)
  • selection: 选择框(当开启行选择时)
  • indentText: 缩进内容(当使用树形数据时)
  • isHovering: 当前行是否处于悬停状态

通过 isHovering 参数,可以实现鼠标悬停时显示操作按钮等交互效果。

标题大小交付状态所有者更新日期
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
代码
import { useState } from 'react';
import { IconTickCircle, IconComment, IconClear, IconDelete } from '@fibrous/semi-icons-react';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrous/semi-illustrations-react';
import { Table, Avatar, Button, Empty, Typography, Tag, type ColumnProps } from '@fibrous/semi-ui-react';
const { Text } = Typography;

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

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

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

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

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

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

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

带分页组件的表格

表格分页目前支持两种模式:受控和非受控模式。

  • 受控模式下,分页的状态完全由外部传入,依据为是否往 Table 传入了 pagination.currentPage 这个字段。一般情况下,受控模式适用于远程拉取数据并渲染。
  • 非受控模式下,Table 默认会将传入的 dataSource 长度作为 total 传给 Pagination 组件,当然你也可以传入一个 total 字段来覆盖 Table 组件的取值,不过我们并不推荐用户在非受控分页模式下传入这个字段。
标题
交付状态所有者
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 条
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

const DAY = 24 * 60 * 60 * 1000;

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

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

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

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

拉取远程数据

正常情况下,数据往往不是一次性获取的,我们会在点击页码、过滤器或者排序按钮时从接口重新获取数据,这种情况下请使用受控模式来处理分页。用户需往 Table 传入 pagination.currentPage 这个字段,此时分页组件的渲染完全依赖于传入的 pagination 对象。

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

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

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

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

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

const data = getData();

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

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

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

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

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

固定列或表头

可以通过设置 column 的 fixed 属性以及 scroll.x 来进行列固定,通过设置 scroll.y 来进行表头固定。

如果是固定值,设置为 >=所有固定列宽之和 + 所有表格列宽之和 的数值。

  • 建议指定 scroll.x 为大于表格宽度的固定值或百分比。如果是固定值,设置为 >=所有固定列宽之和+所有表格列宽之和 的数值。
  • 若列头与内容不对齐或出现列重复或者固定列失效的情况,请指定固定列的宽度 width,若指定宽度后仍不生效,请尝试建议留一列不设宽度以适应弹性布局,或者检查是否有超长连续字段破坏布局。
  • 请确保表格内部的所有元素在渲染后不会对单元格的高度造成影响(例如含有未加载完成的图片等),这种情况下请给定子元素一个确定的高度,以此确保左右固定列单元格不会错乱。
标题
所有者
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 条
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

通过 sticky 属性可以将表头固定在页面顶部。传入 top 时可以控制距离滚动容器的距离。

开启 sticky 后,Table 会自动打开 fixed 布局,列宽将由 column.width 决定。没有给定 width 的列宽由浏览器自动分配。

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

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

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

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

    const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
    const rowSelection = useMemo(
        () => ({
            onChange: (selectedRowKeys, selectedRows) => {
                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
            },
            getCheckboxProps: (record) => ({
                disabled: record.name === 'Michael James', // 这一行不可勾选
                name: record.name,
            }),
            fixed: true,
        }),
        []
    );

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

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

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

render(App);

带排序和过滤功能的表头

表格内部集成了过滤器和排序控件,用户可以通过在 Column 中传入 filters 以及 onFilter 开启表头的过滤器控件展示,传入 sorter 开启表头的排序控件的展示。

交付状态所有者
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 条
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

sorter 为函数类型时,可以通过函数的第三个参数获取 sortOrder 状态。函数类型为 (a?: RecordType, b?: RecordType, sortOrder?: 'ascend' | 'descend') => number。

可通过 showSortTip 属性控制是否展示排序提示,默认为 false。当开启提示后,当仅有排序功能时候,鼠标移动至表头时,会展示排序提示;其他情况下,仅鼠标移动至排序图标时,会展示排序提示。

注:在使用 sortOrder 属性受控排序时,由于无法预测下一个排序顺序,因此 showSortTip 不生效,不会展示提示。

标题
所有者更新日期
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 条
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

自定义表头筛选

如果你需要将筛选器输入框展示在表头,可在 title 传入 ReactNode,配合 filteredValue 使用。

标题
交付状态所有者
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 条
代码
import { useState, useRef, type CompositionEvent } from 'react';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-react';
import { Table, Avatar, Input, Space, Tag, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

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

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

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

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

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

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

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

自定义筛选器

使用 renderFilterDropdown 自定义渲染筛选器面板。

你可以在用户输入筛选值的时候调用 setTempFilteredValue 存储筛选值,在筛选值输入完毕后调用 confirm 触发真正的筛选。也可以通过 confirm({ filteredValue }) 直接筛选。

设置 tempFilteredValue 的原因是在需要存储临时筛选值的场景,不需要自己声明一个 state 保存这个临时筛选值。

type RenderFilterDropdown = (props: RenderFilterDropdownProps) => React.ReactNode;
interface RenderFilterDropdownProps {
    /** 临时筛选值,初始值为 `filteredValue` 或 `defaultFilteredValue`  */
    tempFilteredValue: any[];
    /** 设置临时筛选值  */
    setTempFilteredValue: (tempFilteredValue: any[]) => void;
    /** `confirm` 默认会将 `tempFilteredValue` 赋值给 `filteredValue` 并触发 `onChange` 事件。你也可以通过传入 `filteredValue` 直接设置筛选值  */
    confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
    /** 清除筛选值、临时筛选值  */
    clear: (props?: { closeDropdown?: boolean }) => void;
    /** 关闭 dropdown  */
    close: () => void;
    /** 把临时筛选值还原为已生效的筛选值  */
    reset: () => void;
    /** 筛选器配置项,如不需要可以不传  */
    filters?: RenderDropdownProps['filters'];
}
标题
所有者
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 条
代码
import { useState, useRef } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Input, Button, Space, type InputHandle, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

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

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

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

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

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

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

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

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

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

筛选确认模式

通过设置 filterConfirmMode='confirm',可以让筛选下拉面板支持确认模式。在该模式下:

  • 点击筛选项不会立即生效,而是先暂存到临时状态
  • 下拉面板底部会显示"确定"和"重置"按钮
  • 点击"确定"按钮后才会应用筛选条件并关闭下拉面板
  • 点击"重置"按钮会恢复到打开下拉面板时的初始状态(不会关闭面板)

这个功能适用于需要多选筛选条件后再一次性应用的场景,避免每次点击都触发筛选。

标题
所有者
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 条
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

const DAY = 24 * 60 * 60 * 1000;

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

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

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

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

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

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

自定义筛选项渲染

支持往 column 中传入 renderFilterDropdownItem 自定义每个筛选项的渲染方式。

  • text: ReactNode 当前筛选项的文案;
  • value: any 当前筛选项的值;
  • checked: boolean 当前筛选项是否已经选中;
  • filteredValue: any[] 当前所有的筛选值;
  • level: number 当前筛选项所处层级,如果是嵌套的筛选项,该值会 >= 1;
  • filterMultiple: boolean 当前筛选项是否为多选。
所有者
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 条
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, Dropdown, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

可以展开的表格

一般可展开行

如果需要渲染可以展开的表格,除了需要在 Table 传 expandedRowRender 这个方法外,还必须要指定 rowKey(默认为 key),Table 会根据 rowKey 取得行唯一标识符。

  • 如果 rowKey 为 Function,则会把 rowKey(record) 的结果作为行唯一 ID
  • 如果 rowKey 为 string 类型,则会把 record[rowKey] 作为行唯一 ID
标题大小所有者更新日期
Semi Design 设计稿.fig2M
姜姜鹏志
2020-02-02 05:13
Semi Design 分享演示文稿2M
郝郝宣
2020-01-17 05:31
设计文档34KB
ZZoey Edwards
2020-01-26 11:01
代码
import type { ReactNode } from 'react';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

展开按钮渲染为单独列

默认情况,展开按钮会与第一列文案渲染在同一个单元格内,你可以将 hideExpandedColumn 设为 false 来渲染为单独一列:

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

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

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

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

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

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

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

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

关闭某一行的可展开按钮渲染

可传入 rowExpandable 方法,入参为 record,判断返回值是否为 false 来关闭某一行的可展开按钮的渲染。

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

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

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

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

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

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

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

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

树形数据展示

表格支持树形数据的展示,当数据中有 children 字段时会自动展示为树形表格,如果不需要或使用其他字段可以用 childrenRecordName 进行配置。另外可以通过设置 indentSize 以控制每一层的缩进宽度。

注意:请务必为每行数据提供一个与其他行值不同的 key,或者使用 rowKey 参数指定一个作为主键的属性名。

树形数据简单示例

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

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

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

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

行可交换的树形数据

你可以通过改变 dataSource 元素的顺序来实现行交换操作。

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

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

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

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

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

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

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

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

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

            setData(newData);
        }
    };

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

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

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

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

            Object.assign(obj, copied);

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

        return obj;
    };

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

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

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

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

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

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

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

树形选择

默认情况下,表格的行选中是各自独立的,你可以通过定义 selectedRowKeys 来模拟一个树形选中。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

树形选择关联(checkRelation)

通过设置 rowSelection.checkRelation 为 'related',可以实现父子节点选择关联。选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选)。

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

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

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

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

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

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

自定义行或单元格事件以及属性

  • 传入 onRow/onHeaderRow 可以定义表格或表头行的原生事件或属性。
  • 传入 column.onCell/column.onHeaderCell 可以定义表格或表头单元格原生事件或属性。

原则上 tr/td/th 上支持的属性或事件都能够被定义。例如下面这个例子:

  • 表头的 tr 定义了 onMouseEnter/onMouseLeave
  • 表格的 tr 定义了 className
  • 表格的第三行定义了 onClick
标题
所有者
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 条
代码
import { useMemo, useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnHeaderRow, type OnRow } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

实现斑马纹样式

使用 onRow 给每行设置一个背景色,实现有斑马纹效果的表格。如果设置了固定列,可以通过 onCell 给每列设置一个背景色实现相同效果。

标题大小所有者更新日期
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
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps, type OnRow } from '@fibrous/semi-ui-react';

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

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

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

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

实现表头样式定制

可以通过 Column.onHeaderCell 返回特定 style 或 className,定制表头的样式
如下例子,通过传入 backgroundColor 改变了表头背景色

标题大小所有者更新日期
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
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

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

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

实现单元格 Hover 样式定制

Table 默认为整行配置 Hover 样式,如果你需要修改相关样式可以通过 CSS 覆盖的方式自行实现。
如下例子,通过 CSS 覆盖,将可 Hover 的背景色或者由行高亮改为 Cell 单元格高亮

.component-table-demo-cell-hover-custom {
    .semi-table-tbody {
        // 去掉行 hover 的原有样式
    .semi-table-row:hover {
            .semi-table-row-cell {
                background-color: transparent;
                background-image: none;
                &.semi-table-cell-fixed-left::before {
                    background-color: transparent;
                }
            }
            // 给单个单元格加样式
      .semi-table-row-cell:hover {
                background-color: var(--semi-light-green-1);
            }
        }
    }
}
标题大小所有者更新日期
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
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

单元格缩略

使用 ellipsis 可以让单元格自动实现缩略效果。

所有者大小更新日期
Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other 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
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

设置 ellipsis.showTitle 为 false 可以隐藏默认原生的 HTML title。配合 column.render 可以自定义内容提示。

所有者大小更新日期
Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other 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
代码
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Typography, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

可伸缩列

基本伸缩列

对于一些内容比较多的列,可以选择打开伸缩列功能,在表头进行拉拽实现列宽的实时变化。

不过你需要注意一些参数:

  • resizable 设定为 true 或者一个 object
  • columns 里需要伸缩功能的列都要指定 width 这个字段(如果不传,该列不具备伸缩功能,且其列宽度会被浏览器自动调整)
  • column.resize 可以在 resizable 开启后生效,设置为 false 后,列不再支持伸缩。
  • 拖动过的列保留拖动后的宽度,没拖动过的列跟随 columns 里的 width

与固定列同时使用时,需指定某一列不设置宽度

不推荐与 scroll.x 同时使用,scroll.x 指定表格是有宽度范围的,而伸缩列会拓展列宽,这可能会导致表格对不齐

标题
所有者
操作列
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 条
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

进阶的伸缩列

resizable 还能为一个 Object,包括三个事件方法:

  • onResize
  • onResizeStart
  • onResizeStop

分别触发于列宽改变中、开始改变和结束改变三个时机。开发者可以选择在这个时机修改 column,例如在拉拽时增加一个拖动时的竖线效果等,如下例。

标题
所有者
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 条
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { addClass, removeClass } from '@fibrous/semi-foundation/utils/classnames';
import { Table, Avatar, type ColumnProps, type TableResizableProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

            return { className };
        },
    };

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

本例中使用的 CSS 样式定义:

#components-table-demo-resizable-column .my-resizing {
    border-right: 2px solid red;
}

#components-table-demo-resizable-column .semi-table-resize-handle:hover {
    background-color: red;
}

#components-table-demo-resizable-column .my-resizing:hover .semi-table-resize-handle {
    background-color: inherit;
}

拖拽排序

使用 dnd-kit 搭配 components API 可轻松实现拖拽排序。

标题
所有者
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 条
代码
import { useMemo, useState, type ComponentProps, type CSSProperties } from 'react';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { arrayMove } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/react';
import { isSortable, useSortable } from '@dnd-kit/react/sortable';
import clsx from 'clsx';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnRowReturnObject, type TableProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

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

表格分组

对于一些数据需要分组展示的表格,可以传入 groupBy 定义分组规则,使用 renderGroupSection 来定义分组表头的渲染。

注意:请务必为每行数据提供一个与其他行值不同的 key,或者使用 rowKey 参数指定一个作为主键的属性名。

标题
所有者
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 条
代码
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

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

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

const data = getData();

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

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

虚拟化表格

虚拟化可用于需要渲染大规模数据的场景,通过配置 virtualized 参数来开启这个功能。需要注意的是:

  • 必须传递 scroll.y(number)与 style.width(number);
  • 需要传递每行的高度 virtualized.itemSize(不传时按 size 取默认行高:default 为 53,middle 为 45,small 为 37,组头行高与普通行相同),可以为如下类型:
    • number
    • (index, { sectionRow?: boolean, expandedRow?: boolean }) => number
  • 表格分组虚拟化已支持
  • virtualized 对象另外支持 overscanCount:可视区上下额外渲染的行数,默认 2

通过 getVirtualizedListRef 获取虚拟列表的 ref,ref.current 提供 scrollTo(offset) 与 scrollToItem(index, align),align 为 auto(默认)、smart、center、start、end。

以下为渲染 1000 条数据的示例。

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

const DAY = 24 * 60 * 60 * 1000;

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

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

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

const data = getData();

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

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

无限滚动

基于虚拟化特性,通过传入 virtualized.onScroll 我们可以实现无限滚动加载数据。

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

const DAY = 24 * 60 * 60 * 1000;

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

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

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

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

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

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

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

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

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

受控的动态表格

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

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

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

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

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

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

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

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

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

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

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

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

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

    return nextColumns;
};

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

    return result;
};

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

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

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

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

    return result;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

完全自定义渲染

一般情况下,使用 Column.render 即可,但是你也可以通过传递 Column.useFullRender=true 来开启完全自定义渲染模式,此时复选框按钮、展开按钮、缩进等组件将会透传至 Column.title 与 Column.render 方法中,你可以进一步来定义表头和单元格的内容渲染方式。

其中 Column.title 接受的入参为:

{
    filter: ReactNode, // 筛选按钮
    sorter: ReactNode, // 排序按钮
    selection: ReactNode, // 选择按钮
}

Column.render 第四个入参为一个 object,结构如下:

{
    expandIcon: ReactNode, // 展开按钮
    selection: ReactNode, // 选择按钮
    indentTex: ReactNode, // 缩进
}

下方的例子则是将复选框与内容渲染至同一单元格和表头中。

Name
所有者
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 条
代码
import { useState, useMemo } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

表头合并

用户可以通过表头合并功能进行表头的分组,表头合并支持与固定列、虚拟化、数据分组、列伸缩等功能复合使用,也同时支持 JSX 或者配置式写法。

合并表头配置式写法

基本信息其他信息更多
标题
所有者
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 条
代码
import { useMemo } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

合并表头 JSX 写法

基本信息其他信息更多
标题
所有者
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 条
代码
import { useMemo, type ReactNode } from 'react';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-react';
import { Table, Avatar } from '@fibrous/semi-ui-react';

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

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

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

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

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

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

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

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

行列合并

  • 表头除了通过 children 写法进行合并外,可通过设置 column.colSpan 进行表头的列合并。
  • 表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。
type Render = (text: string, record: Object, index: number, options?: RenderOptions) => {
    children: React.ReactNode;
    // 合并进 td 的属性,colSpan 或 rowSpan 为 0 时不渲染该单元格
    props: React.TdHTMLAttributes<HTMLElement>,
}

interface RenderOptions {
    expandIcon?: React.ReactNode;
    isHovering?: boolean;
}
标题大小所有者
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
代码
import { useState } from 'react';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RenderReturnObject } from '@fibrous/semi-ui-react';

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

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

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

const DAY = 24 * 60 * 60 * 1000;

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

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

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

API 参考

Table

Props

属性说明类型默认值
bordered是否展示外边框和列边框。booleanfalse
childrenTable.Column 子项,未传入 columns 时读取它们。ReactNode-
childrenRecordName树形表格中,数据项里表示子级数据的字段名。string"children"
className最外层容器的类名。string""
clickGroupedRowToExpand点击分组行时展开或收起该组。boolean-
columns列配置,详见 Column。传入时(包括空数组)优先于 Table.Column 子项。TypeColumnProps<RecordType>[]-
components覆盖 Table 的组成元素,如 table、header.cell、body.row,值为标签名或组件;组件接收被替换元素的属性。TableComponents-
dataSource数据。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。RecordType[][]
defaultExpandAllGroupRows默认是否展开所有分组行,动态加载数据时不生效。booleanfalse
defaultExpandAllRows默认是否展开所有行,动态加载数据时不生效。booleanfalse
defaultExpandedRowKeys默认展开的行 key 数组,动态加载数据时不生效。(string | number)[][]
direction文字方向,默认值取 ConfigProvider 的 direction,可在此单独配置。Direction-
empty无数据时展示的内容,默认为语言包中的空状态文案。ReactNode-
expandAllGroupRows是否展开所有分组行。booleanfalse
expandAllRows是否展开所有行。booleanfalse
expandCellFixed展开按钮所在列是否固定,取值与 Column 的 fixed 相同。Fixedfalse
expandedRowKeys展开的行 key 数组,传入后行展开受控。(string | number)[]-
expandedRowRender额外的展开行。返回元素,或返回 { children, fixed } 形式的对象设置展开行的固定方式。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。TypeExpandedRowRender<RecordType>-
expandIcon传 false 隐藏展开按钮;传元素或函数替换展开图标,函数接收 expanded。ExpandIcon-
expandRowByClick点击行时是否展开可展开行。booleanfalse
footer表格尾部,可以是元素,或接收当前页数据返回元素的函数。TypeFooter<RecordType>-
getVirtualizedListRef虚拟化列表创建后调用,参数为列表的 ref 对象,其 current 上提供 scrollTo 与 scrollToItem。仅在配置 virtualized 时有效。GetVirtualizedListRef-
groupBy分组依据,可以是数据项中的字段名,或返回字符串、数字的函数。TypeGroupBy<RecordType>-
headerStyle所有表头单元格(包括固定表头)的内联样式。React.CSSProperties-
hideExpandedColumn表格可展开时,展开按钮默认与第一列内容渲染在同一个单元格;设为 false 时展开按钮单独占一列。booleantrue
id最外层容器的 id。string-
indentSize树形数据每一层的缩进宽度,单位为像素。number20
keepDOM折叠行时是否保留被折叠的 DOM。boolean-
loading是否显示加载状态。booleanfalse
onChange分页、排序、筛选变化时触发,参数 changeInfo 包含 pagination、filters、sorter 与 extra,extra.changeType 为 sorter、filter 或 pagination。TypeOnChange<RecordType>-
onExpand点击行展开图标时触发,参数为是否展开、该行数据与鼠标事件。TypeOnExpand<RecordType>-
onExpandedRowsChange展开的行变化时触发,参数为展开行的数据数组。TypeOnExpandedRowsChange<RecordType>-
onGroupedRow类似于 onRow,单独用于分组行的属性与事件,返回的对象会合并到分组行的 tr 上。index 在未开启 virtualized 时是分组序号,开启后是该行在虚拟列表中的位置。TypeOnGroupedRow<RecordType>-
onHeaderRow设置表头行的属性与事件,返回的对象会合并到表头行的 tr 上(不含 style,请用 headerStyle)。TypeOnHeaderRow<RecordType>-
onRow设置数据行的属性与事件,第三个参数为行状态(disabled 与 selected)。返回的对象会合并到数据行的 tr 上。TypeOnRow<RecordType>-
onRowContextMenu数据行触发右键菜单时的回调,先于 onRow 返回的 onContextMenu 调用,两者都会执行。Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
onRowDoubleClick双击数据行时的回调,先于 onRow 返回的 onDoubleClick 调用,两者都会执行。Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
onRowMouseEnter鼠标进入数据行时的回调,先于 onRow 返回的 onMouseEnter 调用,两者都会执行。Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
onRowMouseLeave鼠标离开数据行时的回调,先于 onRow 返回的 onMouseLeave 调用,两者都会执行。Type(record: RecordType, e: React.MouseEvent<HTMLElement>) => void-
pagination分页配置,传 false 关闭分页。TablePaginationtrue
prefixCls样式名前缀。string"semi-table"
ref表格的句柄,提供 getCurrentPageData 方法,见下方方法。TypeReact.Ref<TableHandle<RecordType>>-
renderGroupSection渲染分组行的内容。返回元素时作为分组行唯一一列的内容;返回 { children, ...列配置 } 对象时,children 是内容,其余字段作为该列的配置。RenderGroupSection-
renderPagination自定义分页器渲染方法,接收分页配置并返回元素。RenderPagination-
resizable是否开启列宽伸缩,需要伸缩的列必须提供 width。传入对象时可配置 onResize、onResizeStart、onResizeStop 与 handlerClassName。TypeResizable<RecordType>-
rowExpandable判断某一行是否可展开,返回 false 时不渲染该行的展开按钮。TypeRowExpandable<RecordType>-
rowKey行 key 的取值:数据项的字段名,或返回字符串、数字的函数。TypeRowKey<RecordType>"key"
rowSelection行选择配置,传 true 使用默认配置,详见 rowSelection。TypeRowSelection<RecordType>null
rowSpanHover悬停在带 rowSpan 的单元格上时,高亮它跨越的所有行。boolean-
scroll滚动区域配置,x 为横向宽度,y 为纵向高度,详见 scroll。Scroll-
showHeader是否显示表头。booleantrue
size表格尺寸,影响单元格的内边距。Size"default"
sticky将表头固定在页面顶部。传对象时 top 为距离滚动容器顶部的距离。Sticky-
style最外层容器的样式。React.CSSProperties-
title表格标题,可以是元素,或接收当前页数据返回元素的函数。TypeTitle<RecordType>-
virtualized开启虚拟化,需同时传 scroll.y(数字)。对象形式可配置行高 itemSize、滚动回调 onScroll 与额外渲染的行数 overscanCount。Virtualized-

Methods

方法说明类型
getCurrentPageData返回当前页的数据(深拷贝),形如 { dataSource, groups };groups 是 Map,键为分组键,值为该组的行键集合。Type() => Pick<BasePageData<RecordType>, 'dataSource' | 'groups'>

aria-label、aria-labelledby、aria-describedby 落在 role="table" 的元素上,其他原生属性(如 data-*、onClick)落在最外层的 div 上。

virtualized 传对象时的字段:

字段说明类型默认值
itemSize每行的高度,不传时按 size 取默认值:default 为 53,middle 为 45,small 为 37,组头行同普通行number | ((index: number, row: { sectionRow?: boolean, expandedRow?: boolean }) => number)53
onScroll虚拟化滚动回调方法,只在纵向滚动偏移变化时调用(object: { scrollDirection?: 'forward' | 'backward', scrollOffset?: number, scrollUpdateWasRequested?: boolean }) => void-
overscanCount可视区上下额外渲染的行数number2

headerStyle 示例

headerStyle 会应用到所有表头 <th> 元素,包括 fixed 表头。

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

const columns = [
    {
        title: '姓名',
        dataIndex: 'name',
        width: 180,
        fixed: 'left',
    },
    {
        title: '年龄',
        dataIndex: 'age',
        width: 120,
    },
    {
        title: '地址',
        dataIndex: 'address',
        width: 280,
    },
    {
        title: '公司',
        dataIndex: 'company',
        width: 280,
    },
];

const dataSource = Array.from({ length: 6 }).map((_, i) => ({
    key: i,
    name: `Edward ${i}`,
    age: 20 + i,
    address: '西湖区湖底公园 1 号',
    company: 'Semi Design',
}));

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

一些上面用到的类型定义:

// PaginationProps 为 Pagination 组件支持的 props
interface TablePaginationProps extends PaginationProps {
    position?: PaginationPosition;
    formatPageText?: FormatPageText;
}

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

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

RecordType 为 Table 和 Column 的泛型参数,默认为 object 类型。你可以这样使用 RecordType:

import { ColumnProps } from 'table/interface';

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

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

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

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

onHeaderRow / onRow 用法

onHeaderRow 中可以返回 th 支持的属性或者事件 onRow 中可以返回 tr 支持的属性或者事件

onRow 的第三个参数 rowStatus 可以获取当前行的状态信息,包括 disabled 和 selected 属性。这在需要根据行的选中或禁用状态执行不同逻辑时非常有用,例如点击行时判断是否允许选中。

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

() => (
    <Table
        onRow={(record, index, rowStatus) => {
            return {
                onClick: (event) => {
                    // rowStatus.disabled 表示当前行是否被禁用
                    // rowStatus.selected 表示当前行是否被选中
                    if (rowStatus?.disabled) {
                        console.log('该行已被禁用');
                        return;
                    }
                    console.log('点击行', index, rowStatus);
                }, // 点击行
                onMouseEnter: (event) => {}, // 鼠标移入行
                onMouseLeave: (event) => {}, // 鼠标移出行
                className: '',
                // ...
                // 其他可以作用于 tr 的属性或事件
            };
        }}
        onHeaderRow={(columns, index) => {
            return {
                onClick: (event) => {}, // 点击表头行
                onMouseEnter: (event) => {}, // 鼠标移入表头行
                onMouseLeave: (event) => {}, // 鼠标移出表头行
                className: '',
                // ...
                // 其他可以作用于 th 的属性或事件
            };
        }}
    />
);

Column

列配置只接受下表的属性,拼错的属性名会报类型错误。需要在列上携带自定义字段时,用交叉类型扩展,如 ColumnProps<RecordType> & { tooltip: string }。

Props

属性说明类型默认值
align列内容的对齐方式,可选 left、right 或 center,RTL 时自动切换。未设置时为 left。Align-
children表头合并时的子列配置。TypeArray<ColumnProps<RecordType>>-
className列样式名。string-
colSpan表头单元格横跨的列数,设置为 0 时不渲染该表头单元格。number-
dataIndex列数据在数据项中对应的字段。使用 sorter 或 onFilter 时必须提供唯一的 dataIndex。string-
defaultFilteredValue筛选的默认值,值为已筛选的 value 数组,非受控时作为初始筛选状态。any[]-
defaultSortOrder排序的默认方向,可选 ascend、descend 或 false,非受控时作为初始排序状态。SortOrder-
ellipsis文本超出单元格时省略显示,开启后表格布局自动切换为 fixed。传对象时,showTitle 设为 false 可隐藏原生 HTML title 提示,配合 render 可自定义内容提示。BaseEllipsis-
filterChildrenRecord树形表格中筛选时是否同时筛选子数据。开启后,子数据符合条件时父数据即使不符合也会保留。boolean-
filterConfirmMode筛选确认模式。immediate 为点击筛选项立即生效;confirm 为点击筛选项后需点击确定按钮才生效,此时下拉面板底部会显示确定和重置按钮。未设置时为 immediate。FilterConfirmMode-
filterDropdown传入元素替换默认的筛选菜单,菜单内的交互由使用方自行编写。ColumnFilterProps['filterDropdown']-
filterDropdownProps透传给 Dropdown 的属性,详见 Dropdown API。ColumnFilterProps['filterDropdownProps']-
filterDropdownVisible控制筛选 Dropdown 的 visible,详见 Dropdown API。boolean-
filteredValue筛选的受控值,值为已筛选的 value 数组。传入后筛选状态由使用方控制。any[]-
filterIcon自定义筛选图标。传元素,或接收 filtered 并返回节点的函数。FilterIcon-
filterMultiple是否允许多选筛选项,未设置时允许多选。boolean-
filters表头的筛选菜单项。使用 renderFilterDropdown 时可以不传。Filter[]-
fixed列是否固定,可选 true(等效于 left)、left 或 right,RTL 时自动切换。Fixed-
key列的标识,用于选择、排序、筛选与列宽;已设置唯一的 dataIndex 时可以省略。string | number-
onCell设置数据单元格的属性与事件,参数为数据项与行索引,返回的对象会合并到 td 上。TypeOnCell<RecordType>-
onFilter本地模式下的筛选函数,接收筛选值与数据项,返回 true 时保留该数据项。使用时必须提供唯一的 dataIndex。TypeOnFilter<RecordType>-
onFilterDropdownVisibleChange筛选下拉菜单显示状态变化时的回调。OnFilterDropdownVisibleChange-
onHeaderCell设置表头单元格的属性与事件,参数为列、列索引与表头行的序号,返回的对象会合并到 th 上。TypeOnHeaderCell<RecordType>-
render自定义单元格内容,参数为 (text, record, index, options)。返回节点,或返回 { children, props } 对象以合并单元格属性(colSpan 为 0 时不渲染该单元格)。options 包含 expandIcon、selection、indentText 与 isHovering。TypeColumnRender<RecordType>-
renderFilterDropdown自定义筛选下拉菜单的内容。参数包含临时筛选值、设置方法,以及 confirm、clear、close 与 reset 方法,详见自定义筛选器。ColumnFilterProps['renderFilterDropdown']-
renderFilterDropdownItem自定义筛选菜单的每一项,参数包含 text、value、checked、onChange、level、filteredValue 与 filterMultiple。RenderFilterDropdownItem-
resize表格开启 resizable 时,传 false 让该列没有伸缩手柄。boolean-
shouldCellUpdate自定义单元格是否需要更新,返回 true 时渲染单元格。默认对单元格的 props 做深对比;数据项较复杂时,可由此接管渲染判断。Type(props: TableCellProps, prevProps: TableCellProps) => boolean-
showSortTip是否展示排序提示,未设置时不展示。设置了 sortOrder 时不生效。boolean-
sortChildrenRecord树形表格中排序时是否同时排序子数据。boolean-
sorter排序函数,参数为两个数据项与当前排序方向,返回负数、0 或正数,与 Array.sort 的比较函数相同。需要服务端排序时设为 true。本地排序时必须提供唯一的 dataIndex,且 dataSource 中每条数据项需有独立的 key。TypeSorter<RecordType>-
sortIcon自定义排序按钮,接收 sortOrder 并返回节点。返回的节点替换整个排序按钮(包含升序和降序),需根据 sortOrder 控制高亮。SortIcon-
sortOrder排序的受控方向,可选 ascend、descend 或 false。传入后排序状态由使用方控制。SortOrder-
title列头内容,可以是节点,或接收 { filter, sorter, selection } 的函数。未开启 useFullRender 时函数以空对象调用,返回值与排序、筛选按钮组合;开启后整个列头由函数的返回值决定。ColumnTitle-
useFullRender是否完全自定义渲染,开启后可在 title 与 render 中读取 sorter、filter、selection 与 expandIcon 等节点,会带来一定的性能损耗。boolean-
width列宽,数字为像素。使用 resizable 时需要提供。string | number-

一些上面用到的类型定义:

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

Column.onCell / onHeaderCell 用法

与 onRow、onHeaderRow类似,在 column.onCell column.onHeaderCell 中也能返回 td/th 支持的属性或事件

rowSelection

属性说明类型默认值
className所处列样式名string
checkRelation父子节点选择关联模式。设置为 'related' 时,选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选);默认为 'unRelated' 即父子节点选择互不影响'related' | 'unRelated''unRelated'
clickRow是否启用点击行选择功能。开启后,点击行任意位置(包括固定列)都会触发行选择/取消选择。被禁用的行(通过 getCheckboxProps 设置)无法通过点击选中;disabled 为 true 时点击行也不会选中。booleanfalse
defaultSelectedRowKeys初始选中项的 key 数组,只在挂载时读取,传了 selectedRowKeys 时不生效(string | number)[]
disabled表头的 Checkbox 是否禁用;为 true 时 clickRow 点击行选择也不生效booleanfalse
fixed把选择框列固定在左边booleanfalse
getCheckboxProps选择框的默认属性配置(record: RecordType) => object
hidden是否隐藏选择列booleanfalse
renderCell自定义渲染勾选框({ selected: boolean, record: RecordType, originNode: JSX.Element, inHeader: boolean, disabled: boolean, indeterminate: boolean, index?: number, selectRow?: (selected: boolean, e: Event) => void, selectAll?: (selected: boolean, e: Event) => void }) => ReactNode
selectedRowKeys指定选中项的 key 数组,需要和 onChange 进行配合string[]
shouldCellUpdate自定义控制单元格是否渲染。默认 cell 会深对比 props 和 nextProps 是否变化,来决定是否渲染单元格。如果你的 props 中的 record 比较复杂,建议使用 shouldCellUpdate 接管单元格的渲染。(props: TableCellProps, prevProps: TableCellProps) => boolean
width自定义列表选择框宽度string|number
onChange选中项发生变化时的回调。第一个参数会保存上次选中的 row keys,即使你做了分页受控或更新了 dataSource FAQ(selectedRowKeys: number[]|string[], selectedRows: RecordType[]) => void
onHeaderCell设置头部单元格属性,index 为表头行的序号(column: Column, columnIndex: number, index: number) => object
onSelect用户手动选择某行的回调。nativeEvent 为选择框的 change 事件;clickRow 点击行时为鼠标事件;经 renderCell 的 selectRow 触发时为传给它的事件(record: RecordType, selected: boolean, selectedRows: RecordType[], nativeEvent: CheckboxEvent | React.MouseEvent | Event) => void
onSelectAll用户手动点击表头选择框的回调,会选中/取消选中当前筛选结果里的所有可选行(不含禁用行),changedRows 只包含选中状态发生变化的行。e 为选择框的 change 事件,经 renderCell 的 selectAll 触发时为传给它的事件(selected: boolean, selectedRows: RecordType[], changedRows: RecordType[], e: CheckboxEvent | Event) => void

scroll

属性说明类型默认值
scrollToFirstRowOnChange当分页、排序、筛选变化后是否自动滚动到表格顶部。当设置了 scroll.y 时,会重置表格内部滚动位置到顶部;当未设置 scroll.y 时,会滚动页面到表格头部位置booleanfalse
x设置横向滚动区域的宽,可以为像素值、百分比或 'max-content'string|number
y设置纵向滚动区域的高,可以为像素值number

pagination

翻页组件配置。pagination 建议不要使用字面量写法。

注意:pagination.onChange 设置后,Table onChange 不再响应分页器变化。

属性说明类型默认值
currentPage当前页码number-
defaultCurrentPage默认的当前页码number1
formatPageText翻页区域文案自定义格式化,传 false 关闭文案显示;该项影响表格翻页区域左侧文案显示,不同于 Pagination 组件的 showTotal 参数,请注意甄别。boolean | ({ currentStart: number, currentEnd: number, total: number }) => string|ReactNodetrue
pageSize每页条数number10
position位置'bottom'|'top'|'both''bottom'
total数据总数number0
preventPageChangeOnPageSizeChange切换 pageSize 时是否阻止自动调整 currentPage。默认情况下,切换 pageSize 时组件会自动计算新的 currentPage 以保持当前数据位置,设为 true 后由用户自行控制页码变化booleanfalse

其他配置详见Pagination

Resizable

resizable 对象型的参数,主要包括一些表格列伸缩时的事件方法。这些事件方法都可以返回一个对象,该对象会和最终的 column 合并。

属性说明类型默认值
handlerClassName列宽调整期间加到该列上的 class 名string'resizing'
onResize表格列改变宽度时触发(column: Column) => Column | void
onResizeStart表格列开始改变宽度时触发(column: Column) => Column | void
onResizeStop表格列停止改变宽度时触发(column: Column) => Column | void

方法

通过 ref 可以访问到 Table 提供的一些内部方法:

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

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

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

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

句柄上的方法见 Table API 中的方法表。

Accessibility

ARIA

Table 按 WAI-ARIA Table 输出静态表格语义,单元格之间不用方向键移动,可交互内容(排序、筛选、展开、选择)都是可以 Tab 聚焦的原生控件。

  • 表格的 role 为 table,行为 row,表头单元格为 columnheader,单元格为 cell;树形表格同样是 table,不输出行的层级
  • 排序按钮是表头里的原生 button,以列标题命名;当前排序方向写在表头单元格的 aria-sort 上(ascending 或 descending),未排序的列不带该属性。showSortTip 的提示在按钮聚焦时显示
  • 展开按钮是原生 button,名称为「展开此行」,aria-expanded 表示该行是否展开
  • 行选择的复选框带有「选择此行」「全选所有行」等可访问名称
  • 默认的列筛选图标是原生 button,可用 Tab 聚焦,Enter、Space 或下箭头打开筛选菜单,并带有 aria-haspopup="menu" 与 aria-expanded;自定义 filterIcon 时由使用方保证其可聚焦
  • 固定表头(scroll.y、sticky)或虚拟化时,表头和表体渲染为两个 table 元素,二者的共同容器是 role 为 table 的元素,内部 table 为 role none,单元格仍与列头关联
  • 虚拟化表格的 aria-rowcount 为表头行数加数据总行数,数据行的 aria-rowindex 接在表头行之后,屏幕阅读器据此报出不在 DOM 中的行

已知限制:

  • 带排序的列,title 渲染在排序按钮里,title 中不要放按钮、链接等可交互内容

RTL/LTR

文案规范

  • 表格标题
    • 表格标题应清晰的让用户感知到表格的目的;
    • 为复杂表格添加描述,为用户提供更多关于表格的上下文信息;
    • 使用句子大小写;
  • 列标题
    • 保持列标题简洁,建议使用 1-2 个词作为列标题;
    • 当列标题较长时,建议 2 行显示,剩余文字缩略并在 Tooltip 中显示完全;
    • 采用 Sentence case 的大小写规则;
    • 列标题使用句子大小写;
  • 表格操作

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$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

  • 点击第二页的行选择按钮,会跳转到第一页?

    Table 的 dataSource 更新后,会将页码重置到初始态。请检查数据源是否在组件渲染时发生了变化。

    function App() {
        const [dataSource, setDataSource] = useState([]);
    
        useEffect(() => {
            // ✅ 正确
            const getData = () => {
                // 获取数据
                const newData = fetch(/**/);
                // 设置数据
                setDataSource(dataSource);
            };
    
            getData();
        }, []);
    
        // ❌ 错误
        const data = [];
    
        return <Table dataSource={data} columns={[/*...*/]} />;
    }
  • 筛选后的数据条数不对?

    请检查你的筛选列和数据源是否配置正确。

    筛选列需设置独立的 dataIndex,同时 dataSource 需要设置独立的 key,请参考 dataSource API。否则筛选功能无法正常工作。

  • 表格数据为何没有更新?

    Table 组件目前所有参数都为浅层对比,也就是说如果该参数值类型为一个 Array 或者 Object,你需要手动改变其引用才能触发更新。同理,如果你不想触发额外更新,尽量不要直接在传参的时候使用字面量或是在 render 过程中定义引用型参数值:

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

    上述的写法在每次 render 时都会触发表格内部对数据的更新(会清空当前的选中行以及展开行 key 数组等)。为了性能及避免一些异常,请尽量将一些引用型参数定义在 render 方法之外(如果使用了 hooks 请利用 useMemo 或者 useState 进行存储)。

  • 为何我的表格行不能选中以及展开?

    请指定 rowKey 或者给 dataSource 的每项设置一个各不相同的 "key" 属性。表格内所有行相关的操作都需要使用到。

  • 如何实现点击排序按钮时自定义排序或传参给服务端排序?

    onChange 方法的入参包括 pagination、filters、sorter,用户可以根据 sorter 对 dataSource 进行自定义排序。

  • 如何给某一行添加 className?

    使用 onRow 或 onHeaderRow。

  • 如何给 table cell 设置样式?

    涉及到单个 cell 需要控制样式的,可以通过 column.onHeaderCell、column.onCell 控制。

  • 为何 rowSelection onChange 的第一个参数缓存了之前选中的 keys?

    这么做为了在分页受控时,在第一页选中数据后,去第二页选择数据,回到第一页后选择的 row keys 丢失的场景。如果不想用缓存的 keys,可以从当前 dataSource 过滤一遍,或者使用 rowSelection onChange 的第二个参数。

  • 支持单行选择吗

    Table 暂不支持单行选择功能,用户可以通过自定义方式实现单选。