如何使用
往 Table 传入表头 columns 和数据 dataSource 进行渲染。
<script lang="ts">
import { Table } from '@fibrae/semi-ui-svelte';
const columns = [
{
title: '标题',
dataIndex: 'name',
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
];
const data = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
},
{
key: '3',
name: '设计文档',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
},
];
</script>
<Table {columns} dataSource={data} pagination={false} />
Svelte 版没有 Table.Column:列只用 columns 数据声明,列里的自定义内容用 Table 的 cell、headerCell 等 snippet 渲染。
代码演示
基本表格
对于表格,最基本的两个参数为 dataSource 和 columns,前者为数据项,后者为每列的配置,二者皆为数组类型。
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| | 2M | 已交付 | 姜 姜鹏志 | 2020-02-02 05:13 | |
| | 2M | 已延期 | 郝 郝宣 | 2020-01-17 05:31 | |
| | 34KB | 待评审 | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconClear, IconComment, IconMore, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, Tag, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
},
];
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',
},
];
</script>
<Table {columns} dataSource={data} pagination={false}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>行选择操作
往 Table 传入 rowSelection 即可打开此功能。
- 点击表头的选择框,会选择
dataSource里所有不是disabled状态的行。选择所有行回调函数为onSelectAll; - 点击行的选择框会选中当前行。它的回调函数为
onSelect; - 按住 Shift 点击行的选择框(开启
clickRow时也可以点击行,或在选择框上按 Shift+Space),会把上一次不按 Shift 点击的行(锚点)到当前行之间的可见行设成与锚点相同的选中状态,跳过禁用的行。锚点不变时,再次按住 Shift 点击会替换上一段范围,范围外的选择保持不变。范围只在当前页内,锚点不在当前页或checkRelation为'related'时按普通点击处理。范围选择只触发一次onChange,不触发onSelect;
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|---|
| | 2M | 已交付 | 姜 姜鹏志 | 2020-02-02 05:13 | ||
| | 2M | 已延期 | 郝 郝宣 | 2020-01-17 05:31 | ||
| | 34KB | 待评审 | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconClear, IconComment, IconMore, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'wait',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi D2C 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: 'Semi D2C 设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'success',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const rowSelection: RowSelectionProps = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
const pagination = { pageSize: 3 };
</script>
<Table {columns} dataSource={data} {pagination} {rowSelection}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>自定义渲染
用户可以使用 Table 的 cell snippet 来自定义单元格的渲染,该功能适用于需要渲染较为复杂的单元格内容时。cell 对所有数据列调用,用 column.dataIndex 或 column.key 区分列,不需要定制的列要在 {:else} 分支里自己渲染 text,否则这些列的单元格是空的。
cell 的参数是一个对象,包含以下属性:
text:column.dataIndex对应的值,没有dataIndex的列为数据项本身record: 当前行的数据项index: 数据项在同级中的序号column: 当前列的配置expandIcon: 展开图标的 snippet(当使用树形数据或可展开行时),在插入处用{@render expandIcon?.()}渲染isHovering: 当前行是否处于悬停状态selection、indentText: 选择框与缩进内容的 snippet,仅在列开启useFullRender时提供,见完全自定义渲染
通过 isHovering 参数,可以实现鼠标悬停时显示操作按钮等交互效果。
<Table {columns} dataSource={data}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<Avatar alt="" size="small" src={record.nameIconSrc} /> {text}
{:else}
{text}
{/if}
{/snippet}
</Table>
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| | 2M | 已交付 | 姜 姜鹏志 | 2020-02-02 05:13 | |
| | 2M | 已延期 | 郝 郝宣 | 2020-01-17 05:31 | |
| | 34KB | 待评审 | Z Zoey Edwards | 2020-01-26 11:01 | |
| | 34KB | 已交付 | 姜 姜琪 | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconClear, IconComment, IconDelete, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrae/semi-illustrations-svelte';
import { Avatar, Button, Empty, Table, Tag, TypographyText, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue' | 'green';
}
const tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const raw: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿标题可能有点长这时候应该显示 Tooltip.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计文档可能也有点长所以也会显示 Tooltip',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'success',
owner: '姜琪',
updateTime: '2020-01-26 11:01',
avatarBg: 'green',
},
];
let dataSource = $state.raw(raw);
const removeRecord = (key: string) => {
dataSource = dataSource.filter((data) => data.key !== key);
};
const resetData = () => {
dataSource = [...raw];
};
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
},
{
title: '大小',
dataIndex: 'size',
width: 150,
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
width: 300,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
},
{
title: '',
dataIndex: 'operate',
},
];
</script>
{#snippet image()}<IllustrationNoResult style="width: 150px; height: 150px" />{/snippet}
{#snippet darkModeImage()}<IllustrationNoResultDark style="width: 150px; height: 150px" />{/snippet}
<div>
<Button style="margin-bottom: 10px" onclick={resetData}>重置</Button>
<Table {columns} {dataSource} pagination={false} style="min-height: 350px">
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<span style="display: flex; align-items: center">
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
<!-- Width = the cell's 400px minus the non-text content -->
<TypographyText ellipsis={{ showTooltip: true }} style="width: calc(400px - 76px)">{text}</TypographyText>
</span>
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<Button aria-label="删除" theme="borderless" onclick={() => removeRecord(record.key)}>
{#snippet icon()}
<IconDelete />
{/snippet}
</Button>
{:else}
{text}
{/if}
{/snippet}
{#snippet empty()}
<Empty {darkModeImage} {image}>
{#snippet description()}搜索无结果{/snippet}
</Empty>
{/snippet}
</Table>
</div>带分页组件的表格
表格分页目前支持两种模式:受控和非受控模式。
- 受控模式下,分页的状态完全由外部传入,依据为是否往 Table 传入了
pagination.currentPage这个字段。一般情况下,受控模式适用于远程拉取数据并渲染。 - 非受控模式下,Table 默认会将传入的
dataSource长度作为total传给 Pagination 组件,当然你也可以传入一个total字段来覆盖 Table 组件的取值,不过我们并不推荐用户在非受控分页模式下传入这个字段。
需要完全自定义分页区域时,传入 paginationContent snippet,它的参数 pagination 可以直接展开给 Pagination,同时替换分页信息文案与分页器。
标题 | 交付状态 | 所有者 |
|---|
| | 0 KB | 已交付 | 姜 姜鹏志 | 2024-05-06 | |
| | 5 KB | 待评审 | 郝 郝宣 | 2024-05-11 | |
| | 10 KB | 已交付 | 姜 姜鹏志 | 2024-05-16 | |
| | 15 KB | 待评审 | 郝 郝宣 | 2024-05-21 | |
| | 20 KB | 已交付 | 姜 姜鹏志 | 2024-05-26 | |
| | 25 KB | 待评审 | 郝 郝宣 | 2024-05-31 | |
| | 30 KB | 已交付 | 姜 姜鹏志 | 2024-06-05 | |
| | 35 KB | 待评审 | 郝 郝宣 | 2024-06-10 | |
| | 40 KB | 已交付 | 姜 姜鹏志 | 2024-06-15 | |
| | 45 KB | 待评审 | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const DAY = 24 * 60 * 60 * 1000;
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: RowSelectionProps = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
</script>
<Table {columns} {dataSource} {rowSelection} {scroll}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>拉取远程数据
正常情况下,数据往往不是一次性获取的,我们会在点击页码、过滤器或者排序按钮时从接口重新获取数据,这种情况下请使用受控模式来处理分页。用户需往 Table 传入 pagination.currentPage 这个字段,此时分页组件的渲染完全依赖于传入的 pagination 对象。
标题 | 交付状态 | 所有者 |
|---|
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, Tag, type ColumnProps } from '@fibrae/semi-ui-svelte';
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 tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
const fetchData = (currentPage: number) => {
return new Promise<Row[]>((res) => {
setTimeout(() => {
res(data.slice((currentPage - 1) * pageSize, currentPage * pageSize));
}, 300);
});
};
let dataSource = $state.raw<Row[]>([]);
let loading = $state(true);
let currentPage = $state(1);
const handlePageChange = (page: number) => {
loading = true;
currentPage = page;
};
$effect(() => {
// Drop responses for pages the user has left
let ignore = false;
void fetchData(currentPage).then((rows) => {
if (!ignore) {
loading = false;
dataSource = rows;
}
});
return () => {
ignore = true;
};
});
</script>
<Table {columns} {dataSource} {loading} pagination={{ currentPage, pageSize, total: data.length, onPageChange: handlePageChange }}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>固定列或表头
可以通过设置 column 的 fixed 属性以及 scroll.x 来进行列固定,通过设置 scroll.y 来进行表头固定。
如果是固定值,设置为 >=所有固定列宽之和 + 所有表格列宽之和 的数值。
- 建议指定
scroll.x为大于表格宽度的固定值或百分比。如果是固定值,设置为>=所有固定列宽之和+所有表格列宽之和的数值。- 若列头与内容不对齐或出现列重复或者固定列失效的情况,请指定固定列的宽度
width,若指定宽度后仍不生效,请尝试建议留一列不设宽度以适应弹性布局,或者检查是否有超长连续字段破坏布局。- 请确保表格内部的所有元素在渲染后不会对单元格的高度造成影响(例如含有未加载完成的图片等),这种情况下请给定子元素一个确定的高度,以此确保左右固定列单元格不会错乱。
标题 | 所有者 |
|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 | ||
| | 5 KB | 郝 郝宣 | 2024-05-11 | ||
| | 10 KB | 姜 姜鹏志 | 2024-05-16 | ||
| | 15 KB | 郝 郝宣 | 2024-05-21 | ||
| | 20 KB | 姜 姜鹏志 | 2024-05-26 | ||
| | 25 KB | 郝 郝宣 | 2024-05-31 | ||
| | 30 KB | 姜 姜鹏志 | 2024-06-05 | ||
| | 35 KB | 郝 郝宣 | 2024-06-10 | ||
| | 40 KB | 姜 姜鹏志 | 2024-06-15 | ||
| | 45 KB | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
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),
},
{
title: '大小',
dataIndex: 'size',
width: 200,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
},
];
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const scroll = { y: 300, x: 1200 };
const rowSelection: RowSelectionProps = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
fixed: true,
};
</script>
<Table {columns} {dataSource} {rowSelection} {scroll}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>通过 sticky 属性可以将表头固定在页面顶部。传入 top 时可以控制距离滚动容器的距离。
开启 sticky 后,Table 会自动打开 fixed 布局,列宽将由 column.width 决定。没有给定 width 的列宽由浏览器自动分配。
带排序和过滤功能的表头
表格内部集成了过滤器和排序控件,用户可以通过在 Column 中传入 filters 以及 onFilter 开启表头的过滤器控件展示,传入 sorter 开启表头的排序控件的展示。
| 交付状态 | 所有者 |
|---|
| | 0 KB | 已交付 | 姜 姜鹏志 | 2024-05-06 | |
| | 5 KB | 待评审 | 郝 郝宣 | 2024-05-11 | |
| | 10 KB | 已交付 | 姜 姜鹏志 | 2024-05-16 | |
| | 15 KB | 待评审 | 郝 郝宣 | 2024-05-21 | |
| | 20 KB | 已交付 | 姜 姜鹏志 | 2024-05-26 | |
| | 25 KB | 待评审 | 郝 郝宣 | 2024-05-31 | |
| | 30 KB | 已交付 | 姜 姜鹏志 | 2024-06-05 | |
| | 35 KB | 待评审 | 郝 郝宣 | 2024-06-10 | |
| | 40 KB | 已交付 | 姜 姜鹏志 | 2024-06-15 | |
| | 45 KB | 待评审 | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
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 tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const dataSource: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
dataSource.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
const rowSelection: RowSelectionProps = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
</script>
<Table {columns} {dataSource} {rowSelection} {scroll}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>sorter 为函数类型时,可以通过函数的第三个参数获取 sortOrder 状态。函数类型为 (a?: RecordType, b?: RecordType, sortOrder?: 'ascend' | 'descend') => number。
可通过 showSortTip 属性控制是否展示排序提示,默认为 false。当开启提示后,当仅有排序功能时候,鼠标移动至表头时,会展示排序提示;其他情况下,仅鼠标移动至排序图标时,会展示排序提示。
注:在使用 sortOrder 属性受控排序时,由于无法预测下一个排序顺序,因此 showSortTip 不生效,不会展示提示。
| 标题 | 所有者 | 更新日期 | |
|---|---|---|---|
| | 3 KB | 姜 姜鹏志 | 2020-02-02 |
| | 未知 | 郝 郝宣 | 2020-01-17 |
| | 1 KB | Z Zoey Edwards | 2020-01-26 |
| | 5 KB | Z Zoey Edwards | 2020-01-26 |
| | 未知 | Z Zoey Edwards | 2020-01-26 |
| | 2 KB | Z Zoey Edwards | 2020-01-26 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size?: number;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const figmaIconUrl = '/assets/placeholder/app.svg';
const docIconUrl = '/assets/docs-icon.png';
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
},
{
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,
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
];
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',
},
];
</script>
<Table {columns} {dataSource}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text ? `${text} KB` : '未知'}
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>自定义表头筛选
如果你需要将筛选器输入框展示在表头,可用 Table 的 headerCell snippet 渲染列标题,配合 filteredValue 使用。列的 title 仍是纯文本,不需要定制的列在 {:else} 分支里渲染 title。
标题 | 所有者 | ||
|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 |
| | 10 KB | 姜 姜鹏志 | 2024-05-16 |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 |
| | 30 KB | 姜 姜鹏志 | 2024-06-05 |
| | 40 KB | 姜 姜鹏志 | 2024-06-15 |
| | 50 KB | 姜 姜鹏志 | 2024-06-25 |
| | 60 KB | 姜 姜鹏志 | 2024-07-05 |
| | 70 KB | 姜 姜鹏志 | 2024-07-15 |
| | 80 KB | 姜 姜鹏志 | 2024-07-25 |
| | 90 KB | 姜 姜鹏志 | 2024-08-04 |
代码
<script lang="ts">
import { tick } from 'svelte';
import { format } from 'date-fns';
import { Avatar, Button, Input, Space, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
let nameInput = $state<Input>();
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
onFilter: (value, record) => record.name.includes(value),
filterDropdown: true,
onFilterDropdownVisibleChange: () => {
// the panel is rendered after the visibility changes
void tick().then(() => nameInput?.focus());
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
{
title: '所有者',
dataIndex: 'owner',
onFilter: (value, record) => record.owner.includes(value),
defaultFilteredValue: ['姜鹏志'],
filterDropdown: true,
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
</script>
<Table {columns} {dataSource}>
{#snippet filterDropdown({ column, tempFilteredValue, onTempFilteredValueChange, onConfirm, onClear, onClose })}
{#if column.dataIndex === 'name'}
<Space align="start" style="padding: 8px" vertical>
<Input bind:this={nameInput} onChange={(value) => onTempFilteredValueChange(value ? [value] : [])} value={tempFilteredValue?.[0]} />
<Space>
<Button onclick={() => onConfirm({ closeDropdown: true })}>筛选 + 关闭</Button>
<Button onclick={() => onClear({ closeDropdown: true })}>清除 + 关闭</Button>
<Button onclick={() => onClose()}>直接关闭</Button>
</Space>
</Space>
{:else}
<Space align="start" style="padding: 8px" vertical>
<Input onChange={(value) => onTempFilteredValueChange(value ? [value] : [])} value={tempFilteredValue?.[0]} />
<Space>
<Button onclick={() => onConfirm({ closeDropdown: false })}>筛选后不关闭</Button>
<Button onclick={() => onClear({ closeDropdown: false })}>清除后不关闭</Button>
<Button onclick={() => onClose()}>直接关闭</Button>
</Space>
</Space>
{/if}
{/snippet}
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>自定义筛选器
把列的 filterDropdown 设为 true,再用 Table 的 filterDropdown snippet 自定义渲染筛选器面板。snippet 对所有 filterDropdown 为 true 的列调用,用 column 区分列。
你可以在用户输入筛选值的时候调用 onTempFilteredValueChange 存储筛选值,在筛选值输入完毕后调用 onConfirm 触发真正的筛选。也可以通过 onConfirm({ filteredValue }) 直接筛选。
interface TableFilterDropdownSnippetProps {
/** 当前列的配置 */
column: ColumnProps;
/** 筛选器配置项,列没有 `filters` 时为 `undefined` */
filters: TableFilter[] | undefined;
/** 临时筛选值,没有暂存值时为已生效的筛选值 */
tempFilteredValue: any[] | undefined;
/** 设置临时筛选值 */
onTempFilteredValueChange: (value?: any[]) => void;
/** `onConfirm` 默认会将临时筛选值作为 `filteredValue` 并触发 `onChange`。你也可以通过传入 `filteredValue` 直接设置筛选值 */
onConfirm: (options?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
/** 清除筛选值、临时筛选值 */
onClear: (options?: { closeDropdown?: boolean }) => void;
/** 关闭 dropdown */
onClose: () => void;
/** 把临时筛选值还原为已生效的筛选值 */
onReset: () => void;
}
filterDropdownProps 中的 onVisibleChange 不生效,筛选面板的开合通过列的 onFilterDropdownVisibleChange 获取。
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| | 0 KB | 已交付 | 姜 姜鹏志 | 2024-01-25 |
| | 5 KB | 待评审 | 郝 郝宣 | 2024-01-30 |
| | 10 KB | 已交付 | 姜 姜鹏志 | 2024-02-04 |
| | 15 KB | 待评审 | 郝 郝宣 | 2024-02-09 |
| | 20 KB | 已交付 | 姜 姜鹏志 | 2024-02-14 |
| | 25 KB | 待评审 | 郝 郝宣 | 2024-02-19 |
| | 30 KB | 已交付 | 姜 姜鹏志 | 2024-02-24 |
| | 35 KB | 待评审 | 郝 郝宣 | 2024-02-29 |
| | 40 KB | 已交付 | 姜 姜鹏志 | 2024-03-05 |
| | 45 KB | 待评审 | 郝 郝宣 | 2024-03-10 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrae/semi-icons-svelte';
import { Avatar, Input, Space, Table, Tag, type ColumnProps } from '@fibrae/semi-ui-svelte';
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 tagConfig = {
success: { color: 'green', icon: IconTickCircle, text: '已交付' },
pending: { color: 'pink', icon: IconClear, text: '已延期' },
wait: { color: 'cyan', icon: IconComment, text: '待评审' },
} as const;
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
let filteredValue = $state<string[]>([]);
// composition makes the Input report the value once an IME composition ends
const handleChange = (value: string) => {
filteredValue = value ? [value] : [];
};
const columns = $derived<ColumnProps[]>([
{
title: '标题',
dataIndex: 'name',
width: 400,
onFilter: (value, record) => record.name.includes(value),
filteredValue,
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
{
title: '交付状态',
dataIndex: 'status',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
]);
</script>
<Table {columns} {dataSource}>
{#snippet headerCell({ column, title })}
{#if column.dataIndex === 'name'}
<Space>
<span>{title}</span>
<Input composition onChange={handleChange} placeholder="请输入筛选值" showClear style="width: 200px" />
</Space>
{:else}
{title}
{/if}
{/snippet}
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'status'}
{@const config = tagConfig[text as Row['status']]}
<Tag color={config.color} shape="circle" style="user-select: text">
{#snippet prefixIcon()}
<config.icon />
{/snippet}
{config.text}
</Tag>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>筛选确认模式
通过设置 filterConfirmMode='confirm',可以让筛选下拉面板支持确认模式。在该模式下:
- 点击筛选项不会立即生效,而是先暂存到临时状态
- 下拉面板底部会显示"确定"和"重置"按钮
- 点击"确定"按钮后才会应用筛选条件并关闭下拉面板
- 点击"重置"按钮会恢复到打开下拉面板时的初始状态(不会关闭面板)
这个功能适用于需要多选筛选条件后再一次性应用的场景,避免每次点击都触发筛选。
标题 | 所有者 | ||
|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-01-25 |
| | 5 KB | 郝 郝宣 | 2024-01-30 |
| | 10 KB | 姜 姜鹏志 | 2024-02-04 |
| | 15 KB | 郝 郝宣 | 2024-02-09 |
| | 20 KB | 姜 姜鹏志 | 2024-02-14 |
| | 25 KB | 郝 郝宣 | 2024-02-19 |
| | 30 KB | 姜 姜鹏志 | 2024-02-24 |
| | 35 KB | 郝 郝宣 | 2024-02-29 |
| | 40 KB | 姜 姜鹏志 | 2024-03-05 |
| | 45 KB | 郝 郝宣 | 2024-03-10 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
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),
},
{
title: '所有者',
dataIndex: 'owner',
filters: [
{
text: '姜鹏志',
value: '姜鹏志',
},
{
text: '郝宣',
value: '郝宣',
},
],
onFilter: (value, record) => record.owner.includes(value),
filterMultiple: true,
filterConfirmMode: 'confirm',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 20; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 首页${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
</script>
<Table {columns} {dataSource}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>自定义筛选项渲染
用 Table 的 filterDropdownItem snippet 自定义每个筛选项的渲染方式,传入后替换所有默认筛选项,用 column 区分列。参数如下:
column: ColumnProps当前列的配置;text: string | undefined当前筛选项的文案;value: any当前筛选项的值;checked: boolean当前筛选项是否已经选中;filteredValue: any[]菜单当前显示的筛选值,确认模式下是尚未生效的临时值;level: number当前筛选项所处菜单的层级,顶层菜单为0;filterMultiple: boolean当前筛选项是否为多选;onChange: (event: MouseEvent) => void切换当前筛选项的选中状态,绑定到筛选项的onclick。
| 所有者 |
|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 | |
| | 5 KB | 郝 郝宣 | 2024-05-11 | |
| | 10 KB | 姜 姜鹏志 | 2024-05-16 | |
| | 15 KB | 郝 郝宣 | 2024-05-21 | |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 | |
| | 25 KB | 郝 郝宣 | 2024-05-31 | |
| | 30 KB | 姜 姜鹏志 | 2024-06-05 | |
| | 35 KB | 郝 郝宣 | 2024-06-10 | |
| | 40 KB | 姜 姜鹏志 | 2024-06-15 | |
| | 45 KB | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, DropdownItem, Table, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: RowSelectionProps = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
</script>
<Table {columns} {dataSource} {rowSelection} {scroll}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
{#snippet filterDropdownItem({ text, checked, onChange })}
<DropdownItem active={checked} onclick={onChange}>{text}</DropdownItem>
{/snippet}
</Table>可以展开的表格
一般可展开行
如果需要渲染可以展开的表格,除了需要为 Table 提供 expandedRow snippet(参数 record、index、expanded,输出为空的行不渲染展开内容)外,还必须要指定 rowKey(默认为 key),Table 会根据 rowKey 取得行唯一标识符。展开的行可以用 bind:expandedRowKeys 受控,初始展开的行直接给 expandedRowKeys 赋初值。
- 如果
rowKey为Function,则会把rowKey(record)的结果作为行唯一 ID - 如果
rowKey为string类型,则会把record[rowKey]作为行唯一 ID
| 标题 | 大小 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | ||
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | ||
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Descriptions, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps[] = [
{ title: '标题', width: 500, dataIndex: 'name' },
{ title: '大小', dataIndex: 'size' },
{ title: '所有者', dataIndex: 'owner' },
{ title: '更新日期', dataIndex: 'updateTime' },
{ title: '', dataIndex: 'operate' },
];
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: string }[]> = {
0: [
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
1: [
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
2: [
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
};
const rowSelection: RowSelectionProps = {
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);
},
};
</script>
<Table {columns} dataSource={data} pagination={false} rowKey="name" {rowSelection}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<span>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</span>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
{#snippet expandedRow({ index })}
<Descriptions align="justify" data={expandData[index]}>
{#snippet value({ item })}
{#if item.key === '垂类标签'}
<Tag style="margin: 0">{item.value}</Tag>
{:else}
{item.value}
{/if}
{/snippet}
</Descriptions>
{/snippet}
</Table>展开按钮渲染为单独列
默认情况,展开按钮会与第一列文案渲染在同一个单元格内,你可以将 hideExpandedColumn 设为 false 来渲染为单独一列:
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | |||
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | |||
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Descriptions, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps[] = [
{ title: '标题', width: 500, dataIndex: 'name' },
{ title: '大小', dataIndex: 'size' },
{ title: '所有者', dataIndex: 'owner' },
{ title: '更新日期', dataIndex: 'updateTime' },
{ title: '', dataIndex: 'operate' },
];
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: string }[]> = {
0: [
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
1: [
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
2: [
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
};
const rowSelection: RowSelectionProps = {
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);
},
};
</script>
<Table {columns} dataSource={data} hideExpandedColumn={false} pagination={false} rowKey="name" {rowSelection}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<span>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</span>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
{#snippet expandedRow({ index })}
<Descriptions align="justify" data={expandData[index]}>
{#snippet value({ item })}
{#if item.key === '垂类标签'}
<Tag style="margin: 0">{item.value}</Tag>
{:else}
{item.value}
{/if}
{/snippet}
</Descriptions>
{/snippet}
</Table>关闭某一行的可展开按钮渲染
可传入 rowExpandable 方法,入参为 record,判断返回值是否为 false 来关闭某一行的可展开按钮的渲染。
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | |||
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | |||
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Descriptions, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps[] = [
{ title: '标题', width: 500, dataIndex: 'name' },
{ title: '大小', dataIndex: 'size' },
{ title: '所有者', dataIndex: 'owner' },
{ title: '更新日期', dataIndex: 'updateTime' },
{ title: '', dataIndex: 'operate' },
];
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: string }[]> = {
0: [
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
1: [
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
2: [
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
};
const rowSelection: RowSelectionProps = {
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 rowExpandable = (record: Record<string, any>) => record.name !== '设计文档';
</script>
<Table {columns} dataSource={data} hideExpandedColumn={false} pagination={false} {rowExpandable} rowKey="name" {rowSelection}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<span>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</span>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
{#snippet expandedRow({ index })}
<Descriptions align="justify" data={expandData[index]}>
{#snippet value({ item })}
{#if item.key === '垂类标签'}
<Tag style="margin: 0">{item.value}</Tag>
{:else}
{item.value}
{/if}
{/snippet}
</Descriptions>
{/snippet}
</Table>树形数据展示
表格支持树形数据的展示,当数据中有 children 字段时会自动展示为树形表格,如果不需要或使用其他字段可以用 childrenRecordName 进行配置。另外可以通过设置 indentSize 以控制每一层的缩进宽度。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
树形数据简单示例
| Key | 名称 | 数据类型 | 描述 | 默认值 |
|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
代码
<script lang="ts">
import { Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface TreeRow {
key?: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
}
const columns: ColumnProps[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
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: '无',
},
],
},
];
</script>
<Table {columns} dataSource={data} defaultExpandAllRows />行可交换的树形数据
你可以通过改变 dataSource 元素的顺序来实现行交换操作。
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| status | 视频状态 | Enum <Integer> 枚举 | 视频的可见、推荐状态 | 1 | |
| vid | 视频 ID | String 字符串 | 标识视频的唯一 ID | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 | |
| title | 视频标题 | String 字符串 | 视频的标题 | 无 | |
| video_description | 视频描述 | String 字符串 | 视频的描述 | 无 |
代码
<script lang="ts">
import { IconArrowDown, IconArrowUp } from '@fibrae/semi-icons-svelte';
import { Button, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface 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';
const columns: ColumnProps[] = [
{
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',
},
];
let expandedRowKeys = $state<(string | number)[]>([1, 2]);
let data = $state.raw(raw);
const getSameLevelRecords = (key: number | undefined, rows: TreeRow[]): TreeRow[] => {
if (key == null) {
return [];
}
if (rows.some((item) => item[rowKey] === key)) {
return rows;
}
for (const item of rows) {
const records = getSameLevelRecords(key, item[childrenRecordName] ?? []);
if (records.length) {
return records;
}
}
return [];
};
// Returns a copy of the tree in which the record swaps places with its sibling `offset` rows away.
const moveRecord = (rows: TreeRow[], key: number | undefined, offset: -1 | 1): TreeRow[] => {
const index = rows.findIndex((item) => item[rowKey] === key);
if (index === -1) {
return rows.map((item) => (item[childrenRecordName] ? { ...item, children: moveRecord(item[childrenRecordName], key, offset) } : item));
}
const next = [...rows];
[next[index], next[index + offset]] = [next[index + offset], next[index]];
return next;
};
const canMove = (record: TreeRow, offset: -1 | 1) => {
const records = getSameLevelRecords(record[rowKey], data);
const index = records.findIndex((item) => item[rowKey] === record[rowKey]);
return index >= 0 && index + offset >= 0 && index + offset < records.length;
};
const move = (record: TreeRow, offset: -1 | 1) => {
data = moveRecord(data, record[rowKey], offset);
};
</script>
<Table bind:expandedRowKeys {childrenRecordName} {columns} dataSource={data} {rowKey}>
{#snippet cell({ column, record, text })}
{#if column.key === 'operation'}
<Button aria-label="上移" disabled={!canMove(record as TreeRow, -1)} onclick={() => move(record as TreeRow, -1)}>
{#snippet icon()}
<IconArrowUp />
{/snippet}
</Button>
<Button aria-label="下移" disabled={!canMove(record as TreeRow, 1)} onclick={() => move(record as TreeRow, 1)}>
{#snippet icon()}
<IconArrowDown />
{/snippet}
</Button>
{:else}
{text}
{/if}
{/snippet}
</Table>树形选择
默认情况下,表格的行选中是各自独立的,你可以通过定义 selectedRowKeys 来模拟一个树形选中。
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
代码
<script lang="ts">
import { pull, union } from 'es-toolkit';
import { Table, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
interface 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 columns: ColumnProps[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
key: 121,
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
// Collects the keys of the rows and all their descendants.
const collectKeys = (rows: TreeRow[]): RowKey[] => rows.flatMap((row) => [row.key, ...collectKeys(row.children ?? [])]);
let selectedRowKeys = $state<RowKey[]>([]);
// Selecting a row selects its descendants too, and clearing it clears them.
const doSelect = (record: Record<string, any>, selected: boolean) => {
const keys = [record.key, ...collectKeys((record as TreeRow).children ?? [])];
selectedRowKeys = selected ? union(selectedRowKeys, keys) : pull([...selectedRowKeys], keys);
};
const doSelectAll = (selected: boolean) => {
selectedRowKeys = selected ? collectKeys(data) : [];
};
const rowSelection = $derived<RowSelectionProps>({
selectedRowKeys,
onSelect: doSelect,
onSelectAll: doSelectAll,
});
</script>
<Table {childrenRecordName} {columns} dataSource={data} pagination={false} {rowKey} {rowSelection} />树形选择关联(checkRelation)
通过设置 rowSelection.checkRelation 为 'related',可以实现父子节点选择关联。选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选)。
| Key | 名称 | 数据类型 | 描述 | |
|---|---|---|---|---|
| videos_info | 视频信息 | Object | 视频的元信息 | |
| text_info | 文本信息 | Object | 文本的元信息 |
代码
<script lang="ts">
import { Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
dataKey: string;
name: string;
type: string;
description: string;
children?: Row[];
}
let selectedRowKeys = $state<(string | number)[]>([]);
const columns: ColumnProps[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 200,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
];
const data: Row[] = [
{
key: '1',
dataKey: 'videos_info',
name: '视频信息',
type: 'Object',
description: '视频的元信息',
children: [
{
key: '1-1',
dataKey: 'status',
name: '视频状态',
type: 'Enum',
description: '视频的可见状态',
},
{
key: '1-2',
dataKey: 'vid',
name: '视频 ID',
type: 'String',
description: '标识视频的唯一 ID',
children: [
{
key: '1-2-1',
dataKey: 'video_url',
name: '视频地址',
type: 'String',
description: '视频的唯一链接',
},
],
},
],
},
{
key: '2',
dataKey: 'text_info',
name: '文本信息',
type: 'Object',
description: '文本的元信息',
children: [
{
key: '2-1',
dataKey: 'title',
name: '标题',
type: 'String',
description: '文本标题',
},
{
key: '2-2',
dataKey: 'description',
name: '描述',
type: 'String',
description: '文本描述',
},
],
},
];
const rowSelection = $derived({
selectedRowKeys,
onChange: (keys: (string | number)[]) => {
selectedRowKeys = keys;
},
checkRelation: 'related' as const,
});
</script>
<Table {columns} dataSource={data} pagination={false} {rowSelection} />自定义行或单元格事件以及属性
- 传入
onRow/onHeaderRow可以定义表格或表头行的原生事件或属性。 - 传入
column.onCell/column.onHeaderCell可以定义表格或表头单元格原生事件或属性。
原则上 tr/td/th 上支持的属性或事件都能够被定义,返回对象使用 Svelte 的原生属性名:class、style(字符串)、data-* 与小写的事件 onclick、onmouseenter、onmouseleave、ondblclick、oncontextmenu 等,colspan、rowspan 也是小写。例如下面这个例子:
- 表头的 tr 定义了
onmouseenter/onmouseleave - 表格的 tr 定义了
class - 表格的第三行定义了
onclick
标题 | 所有者 | ||
|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 |
| | 5 KB | 郝 郝宣 | 2024-05-11 |
| | 10 KB | 姜 姜鹏志 | 2024-05-16 |
| | 15 KB | 郝 郝宣 | 2024-05-21 |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 |
| | 25 KB | 郝 郝宣 | 2024-05-31 |
| | 30 KB | 姜 姜鹏志 | 2024-06-05 |
| | 35 KB | 郝 郝宣 | 2024-06-10 |
| | 40 KB | 姜 姜鹏志 | 2024-06-15 |
| | 45 KB | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps, type TableProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
const onRow: TableProps['onRow'] = (record, index) => {
const props = {
class: 'my-tr-class',
};
if (index === 2) {
return {
...props,
onclick: () => console.log('mouse click: ', record, index),
};
}
return props;
};
const onHeaderRow: TableProps['onHeaderRow'] = (headerColumns, index) => ({
onmouseenter: () => console.log('mouse enter: ', headerColumns, index),
onmouseleave: () => console.log('mouse leave: ', headerColumns, index),
});
</script>
<Table {columns} dataSource={data} {onHeaderRow} {onRow}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>实现斑马纹样式
使用 onRow 给每行设置一个背景色,实现有斑马纹效果的表格。如果设置了固定列,可以通过 onCell 给每列设置一个背景色实现相同效果。
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | |
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | |
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 | |
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | |
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | |
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, type ColumnProps, type TableProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
},
];
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: TableProps['onRow'] = (_record, index) => (index % 2 === 0 ? { style: 'background: var(--semi-color-fill-0)' } : {});
</script>
<Table {columns} dataSource={data} onRow={handleRow} pagination={false}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' ? text.slice(0, 1) : ''}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>实现表头样式定制
可以通过 Column.onHeaderCell 返回特定 style(字符串)或 class,定制表头的样式
如下例子,通过传入 background-color 改变了表头背景色
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | |
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | |
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const onHeaderCell = () => ({
style: 'background-color: var(--semi-color-fill-0)',
});
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 280,
},
{
title: '大小',
width: 100,
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
},
].map((column) => ({ ...column, 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',
},
];
</script>
<Table {columns} dataSource={data} pagination={false}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>实现单元格 Hover 样式定制
Table 默认为整行配置 Hover 样式,如果你需要修改相关样式可以通过 CSS 覆盖的方式自行实现。
如下例子,通过 CSS 覆盖,将可 Hover 的背景色或者由行高亮改为 Cell 单元格高亮
.component-table-demo-cell-hover-custom {
.semi-table-tbody {
// 去掉行 hover 的原有样式
.semi-table-row:hover {
.semi-table-row-cell {
background-color: transparent;
background-image: none;
&.semi-table-cell-fixed-left::before {
background-color: transparent;
}
}
// 给单个单元格加样式
.semi-table-row-cell:hover {
background-color: var(--semi-light-green-1);
}
}
}
}
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| | 2M | 姜 姜鹏志 | 2020-02-02 05:13 | |
| | 2M | 郝 郝宣 | 2020-01-17 05:31 | |
| | 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
},
{
title: '大小',
width: 100,
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
},
];
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',
},
];
</script>
<Table class="component-table-demo-cell-hover-custom" {columns} dataSource={data} pagination={false}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={record.nameIconSrc} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>单元格缩略
使用 ellipsis 可以让单元格自动实现缩略效果。
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 2M | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 2M | 2020-02-02 05:13 | |
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 34KB | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 34KB | 2020-02-02 05:13 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
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,
},
];
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',
},
];
const scroll = { x: 1200 };
</script>
<Table {columns} dataSource={data} pagination={false} {scroll}>
{#snippet cell({ column, text })}
{#if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>设置 ellipsis.showTitle 为 false 可以隐藏默认原生的 HTML title。配合 cell snippet 可以自定义内容提示。
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 2M | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 2M | 2020-02-02 05:13 | |
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang | 34KB | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣 | 34KB | 2020-02-02 05:13 |
代码
<script lang="ts">
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Table, TypographyText, type ColumnProps } from '@fibrae/semi-ui-svelte';
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 },
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
ellipsis: { showTitle: false },
},
{
title: '大小',
dataIndex: 'size',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
ellipsis: true,
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
},
];
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',
},
];
const scroll = { x: 1200 };
</script>
<Table {columns} dataSource={data} pagination={false} {scroll}>
{#snippet cell({ column, text })}
{#if column.dataIndex === 'name' || column.dataIndex === 'owner'}
<TypographyText ellipsis={{ showTooltip: true }}>
{text}
</TypographyText>
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>可伸缩列
基本伸缩列
对于一些内容比较多的列,可以选择打开伸缩列功能,在表头进行拉拽实现列宽的实时变化。
不过你需要注意一些参数:
resizable设定为true或者一个objectcolumns里需要伸缩功能的列都要指定width这个字段(如果不传,该列不具备伸缩功能,且其列宽度会被浏览器自动调整)column.resize可以在 resizable 开启后生效,设置为 false 后,列不再支持伸缩。- 拖动过的列保留拖动后的宽度,没拖动过的列跟随
columns里的width
与固定列同时使用时,需指定某一列不设置宽度
不推荐与
scroll.x同时使用,scroll.x 指定表格是有宽度范围的,而伸缩列会拓展列宽,这可能会导致表格对不齐
标题 | 所有者 | 操作列 | ||
|---|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 | |
| | 5 KB | 郝 郝宣 | 2024-05-11 | |
| | 10 KB | 姜 姜鹏志 | 2024-05-16 | |
| | 15 KB | 郝 郝宣 | 2024-05-21 | |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 | |
| | 25 KB | 郝 郝宣 | 2024-05-31 | |
| | 30 KB | 姜 姜鹏志 | 2024-06-05 | |
| | 35 KB | 郝 郝宣 | 2024-06-10 | |
| | 40 KB | 姜 姜鹏志 | 2024-06-15 | |
| | 45 KB | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
resize: false,
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),
},
{
title: '所有者',
width: 200,
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
{
title: '操作列',
dataIndex: 'operate',
fixed: 'right',
width: 100,
resize: false,
},
];
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
</script>
<Table bordered {columns} dataSource={data} resizable>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
</Table>进阶的伸缩列
resizable 还能为一个 Object,包括三个事件方法:
onResizeonResizeStartonResizeStop
分别触发于列宽改变中、开始改变和结束改变三个时机。开发者可以选择在这个时机修改 column,例如在拉拽时增加一个拖动时的竖线效果等,如下例。
标题 | 所有者 | ||
|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 |
| | 5 KB | 郝 郝宣 | 2024-05-11 |
| | 10 KB | 姜 姜鹏志 | 2024-05-16 |
| | 15 KB | 郝 郝宣 | 2024-05-21 |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { addClass, removeClass } from '@fibrae/semi-foundation/utils/classnames';
import { Avatar, Table, type ColumnProps, type TableResizableProps } from '@fibrae/semi-ui-svelte';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pagination = { pageSize: 5 };
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '所有者',
width: 200,
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
const resizable: TableResizableProps = {
onResizeStart: (curColumn) => ({ class: addClass(curColumn.class, 'my-resizing') }),
onResizeStop: (curColumn) => ({ class: removeClass(curColumn.class, 'my-resizing') }),
};
</script>
<div id="components-table-demo-resizable-column">
<Table bordered {columns} dataSource={data} {pagination} {resizable}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>
</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;
}
拖拽排序
Svelte 版的 components 只接受标签名,不能给每一行传入一个组件,所以不能在行里挂 @dnd-kit/svelte 的 createSortable。示例改用原生 HTML5 拖放:onRow 为每行返回 draggable、ondragstart、ondragenter、ondragover、ondragend,拖到另一行上方时按行 key 调整 dataSource。原生拖放只支持指针操作,需要键盘排序时请改用带键盘支持的排序库。
标题 | 所有者 | ||
|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 |
| | 5 KB | 郝 郝宣 | 2024-05-06 |
| | 10 KB | 姜 姜鹏志 | 2024-05-06 |
| | 15 KB | 郝 郝宣 | 2024-05-06 |
| | 20 KB | 姜 姜鹏志 | 2024-05-06 |
| | 25 KB | 郝 郝宣 | 2024-05-06 |
| | 30 KB | 姜 姜鹏志 | 2024-05-06 |
| | 35 KB | 郝 郝宣 | 2024-05-06 |
| | 40 KB | 姜 姜鹏志 | 2024-05-06 |
| | 45 KB | 郝 郝宣 | 2024-05-06 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps, type TableChangeInfo, type TableProps } from '@fibrae/semi-ui-svelte';
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const pageSize = 10;
let dataSource = $state.raw(getData());
let pageNum = $state(1);
const pageData = $derived(dataSource.slice((pageNum - 1) * pageSize, pageNum * pageSize));
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
// HTML5 drag and drop through the onRow attributes: Table takes only a tag name for its rows, so a sortable library
// cannot wrap each row in a component of its own. Known limit: pointer only; use a sortable library with keyboard
// support when rows must be sortable from the keyboard.
let draggingKey = $state<string | null>(null);
const moveTo = (targetKey: string) => {
const from = dataSource.findIndex((row) => row.key === draggingKey);
const to = dataSource.findIndex((row) => row.key === targetKey);
if (from < 0 || to < 0 || from === to) {
return;
}
const next = [...dataSource];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
dataSource = next;
};
const onRow: TableProps['onRow'] = (record) => ({
draggable: true,
style: `cursor: grab; opacity: ${draggingKey === record.key ? 0.5 : 1}`,
ondragstart: () => (draggingKey = record.key),
ondragenter: (event) => {
event.preventDefault();
moveTo(record.key);
},
ondragover: (event) => event.preventDefault(),
ondragend: () => (draggingKey = null),
});
const handleChange = ({ pagination }: TableChangeInfo) => {
if (pagination?.currentPage) {
pageNum = pagination.currentPage;
}
};
</script>
<Table {columns} dataSource={pageData} onChange={handleChange} {onRow} pagination={{ currentPage: pageNum, pageSize, total: dataSource.length }} rowKey="key">
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>表格分组
对于一些数据需要分组展示的表格,可以传入 groupBy 定义分组规则,使用 groupSection snippet(参数 groupKey、group)来定义分组表头的渲染;分组行唯一一列的列配置由 groupSectionColumn 返回。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
标题 | 所有者 |
|---|
根据文件大小分组 100 KB | |||
根据文件大小分组 112 KB | |||
根据文件大小分组 105 KB | |||
根据文件大小分组 117 KB |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = ((i * 1000) % 19) + 100;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
const scroll = { y: 480 };
const rowKey = (record: Record<string, any>) => `${record.owner.toLowerCase()}-${record.name.toLowerCase()}`;
const onGroupedRow = (group: { groupKey: string | number; records: Record<string, any>[] }, index: number) => ({
onclick: () => {
console.log(`Grouped row clicked: `, group, index);
},
});
</script>
<div style="padding: 20px 0px">
<!-- clickGroupedRowToExpand: a click anywhere on the group row expands it -->
<Table clickGroupedRowToExpand {columns} dataSource={data} groupBy="size" {onGroupedRow} {rowKey} {scroll}>
{#snippet groupSection({ groupKey })}
<strong>根据文件大小分组 {groupKey} KB</strong>
{/snippet}
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>
</div>虚拟化表格
虚拟化可用于需要渲染大规模数据的场景,通过配置 virtualized 参数来开启这个功能。需要注意的是:
- 必须传递
scroll.y(number);表格宽度取style.width,未设置时取scroll.x。style是落在根元素上的样式字符串,width写成带单位的值,如'width: 750px';
- 需要传递每行的高度
virtualized.itemSize(不传时按size取默认行高:default为53,middle为45,small为37,组头行高与普通行相同),可以为如下类型:number(index, { sectionRow?: boolean, expandedRow?: boolean }) => number
- 表格分组虚拟化已支持
virtualized对象另外支持overscanCount:可视区上下额外渲染的行数,默认 2
通过 bind:this 获取 Table 的实例(类型 TableHandle),调用 scrollTo(offset) 与 scrollToItem(index, align),align 为 auto(默认)、smart、center、start、end;表格没有开启虚拟化时调用会在开发环境给出警告。
以下为渲染 1000 条数据的示例。
标题 | 所有者 |
|---|
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Button, Table, type ColumnProps, type TableHandle } from '@fibrae/semi-ui-svelte';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps[] = [
{
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
fixed: 'right',
width: 150,
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 1000; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
const scroll = { y: 400, x: 900 };
const style = 'width: 750px; margin: 0 auto';
let table = $state<TableHandle>();
</script>
<Button onclick={() => table?.scrollToItem(100)}>Scroll to 100</Button>
<Table bind:this={table} {columns} dataSource={data} pagination={false} {scroll} {style} virtualized>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' ? text.slice(0, 1) : ''}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>无限滚动
基于虚拟化特性,通过传入 onScroll 我们可以实现无限滚动加载数据。参数只有 scrollOffset,只在纵向滚动偏移变化时触发,需要滚动方向时自行记录上一次的偏移量比较。
标题 | 所有者 |
|---|
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps, type TableVirtualizedProps, type TableVirtualizedScrollArgs } from '@fibrae/semi-ui-svelte';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps[] = [
{
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
fixed: 'right',
width: 150,
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const pageSize = 20; // 每次加载 20 条
const getPage = (start: number) => {
const page: Row[] = [];
for (let i = start; i < start + pageSize; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
page.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return page;
};
let data = $state.raw(getPage(0));
const scroll = { y: 600, x: 1000 };
const style = 'width: 750px; margin: 0 auto';
const itemSize = 56;
const virtualized: TableVirtualizedProps = { itemSize };
const loadMore = () => {
data = [...data, ...getPage(data.length)];
};
// onScroll reports the offset only, so the scroll direction is kept here
let lastOffset = 0;
const onScroll = ({ scrollOffset }: TableVirtualizedScrollArgs) => {
const forward = scrollOffset > lastOffset;
lastOffset = scrollOffset;
if (forward && scrollOffset >= (data.length - Math.ceil(Number(scroll.y) / itemSize) * 1.5) * itemSize) {
loadMore();
}
};
</script>
<Table {columns} dataSource={data} {onScroll} pagination={false} {scroll} {style} {virtualized}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<div>{text}</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>受控的动态表格
标题 | 所有者 |
|---|
代码
<script lang="ts">
import { onMount } from 'svelte';
import { format } from 'date-fns';
import { Avatar, Button, ButtonGroup, Switch, Table, type ColumnProps, type RowSelectionProps, type TableChangeInfo, type TablePaginationPosition, type TablePaginationProps, type TableScroll } from '@fibrae/semi-ui-svelte';
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[];
}
interface SwitchItem {
text: string;
/** Absent: the switch keeps its own state */
checked?: () => boolean;
onChange: (checked: boolean) => void;
}
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const dataTotalSize = 46;
const wrapStyle = 'margin-bottom: 15px; display: flex; justify-content: space-around; flex-wrap: wrap';
const switchStyle = 'display: inline-flex; align-items: center; margin: 5px';
const initialColumns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
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(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
description: `这是第 ${i} 份设计稿的详细说明。`,
};
});
const defaultPagination: PageState = {
currentPage: 1,
pageSize: 8,
total: allData.length,
};
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.toSorted(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);
let loading = $state(true);
let columns = $state.raw(initialColumns);
let scroll = $state.raw<TableScroll>({});
let rowSelection = $state.raw<RowSelectionProps | undefined>();
let expandable = $state(false);
let expandCellFixed = $state(false);
let showTitle = $state(false);
let showFooter = $state(false);
let expandedRowKeys = $state<(string | number)[]>([]);
let showHeader = $state(true);
let bordered = $state(false);
let resizable = $state(false);
let pagination = $state.raw<PageState | false>(defaultPagination);
let dataSource = $state.raw<Row[]>([]);
const load = (currentPagination: PageState, currentPage?: number, sorter?: ColumnProps, filters?: ColumnProps[]) => {
fetchData(currentPagination, currentPage, sorter, filters)
.then((result) => {
let nextColumns = mergeColumns(result.sorter, columns);
for (const filterObj of result.filters) {
nextColumns = mergeColumns(filterObj, nextColumns);
}
columns = nextColumns;
loading = false;
pagination = result.pagination;
dataSource = result.dataSource;
})
.catch((err) => {
console.error(err);
loading = false;
});
};
const setPage = (page?: number | null, sorter?: ColumnProps | null, filters?: ColumnProps[]) => {
if (loading) {
return;
}
const currentPage = typeof page === 'number' ? page : (pagination && pagination.currentPage) || 1;
loading = true;
load(pagination || defaultPagination, currentPage, sorter || getSorterColumn(columns), filters || getFilterColumns(columns));
};
onMount(() => {
load(defaultPagination, 1, getSorterColumn(initialColumns), []);
});
const tablePagination = $derived(pagination && { ...pagination, onPageChange: setPage });
const toggleFixHeader = (checked: boolean) => {
scroll = { ...scroll, y: checked ? 300 : undefined };
};
const toggleFixColumns = (checked: boolean) => {
const lastIndex = columns.length - 1;
if (checked) {
columns = columns.map((column, index) => {
if (index === 0) {
return { ...column, fixed: true };
}
return index === lastIndex ? { ...column, fixed: 'right' } : column;
});
} else {
columns = columns.map((column) => ({ ...column, fixed: false }));
}
if (rowSelection) {
rowSelection = { ...rowSelection, fixed: checked };
}
scroll = { ...scroll, x: checked ? '150%' : undefined };
expandCellFixed = checked;
};
const toggleRowSelection = (checked: boolean) => {
rowSelection = checked
? {
width: 48,
fixed: true,
onChange: (selectedRowKeys, selectedRows) => console.log('Selection changed, selectedRowKeys: ', selectedRowKeys, 'selectedRows: ', selectedRows),
}
: undefined;
};
const toggleShowSorter = (checked: boolean) => {
columns = columns.map((column) => ({
...column,
sorter: checked ? initialColumns.find((item) => item.dataIndex === column.dataIndex)?.sorter : undefined,
}));
};
const toggleShowFilter = (checked: boolean) => {
if (checked) {
columns = columns.map((column) =>
column.dataIndex === 'name'
? {
...column,
filters: [
{
text: '姓名中包含 1',
value: '1',
},
{
text: '姓名中包含 2',
value: '2',
},
{
text: '姓名中包含 3',
value: '3',
},
],
filteredValue: [],
}
: column
);
} else {
columns = columns.map((column) => ({ ...column, filters: undefined, filteredValue: undefined }));
setPage(null, null, []);
}
};
const onChange = (data: TableChangeInfo) => {
console.log('Table changed: ', data);
const { pagination: pageInfo, sorter, filters } = data;
setPage(pageInfo?.currentPage, sorter, filters);
};
const toggleExpandedRowKeys = (checked: boolean) => {
if (checked) {
expandedRowKeys = dataSource.filter((data) => data.key).map((data) => data.key);
expandable = true;
} else {
expandedRowKeys = [];
}
};
const toggleResizable = (checked: boolean) => {
resizable = checked;
bordered = checked;
};
const toggleDataSource = (checked: boolean) => {
if (checked) {
dataSource = [];
} else {
setPage();
}
};
const switchPagination = (position: TablePaginationPosition | false) => {
pagination = position ? { ...defaultPagination, ...pagination, position } : false;
};
const switches: SwitchItem[] = [
{ text: '固定表头:', checked: () => !!scroll.y, onChange: toggleFixHeader },
{
text: '隐藏表头:',
onChange: (checked) => {
showHeader = !checked;
},
},
{
text: '显示标题:',
onChange: (checked) => {
showTitle = checked;
},
},
{
text: '显示底部:',
onChange: (checked) => {
showFooter = checked;
},
},
{ text: '固定列:', onChange: toggleFixColumns },
{ text: '显示选择列:', onChange: toggleRowSelection },
{
text: '显示加载状态:',
checked: () => loading,
onChange: (checked) => {
loading = checked;
},
},
{ text: '无数据:', checked: () => !dataSource.length, onChange: toggleDataSource },
{
text: '开启排序功能:',
checked: () => columns.some((column) => column.sorter),
onChange: toggleShowSorter,
},
{
text: '开启过滤功能:',
checked: () => columns.some((column) => column.filters),
onChange: toggleShowFilter,
},
{
text: '开启行展开功能:',
checked: () => expandable,
onChange: (checked) => {
expandable = checked;
},
},
{ text: '展开当前所有行:', onChange: toggleExpandedRowKeys },
{
text: '显示边框:',
checked: () => bordered,
onChange: (checked) => {
bordered = checked;
},
},
{ text: '开启列伸缩功能:', onChange: toggleResizable },
];
</script>
{#snippet expandedRow({ record }: { record: Record<string, any> })}
<p>{record.description}</p>
{/snippet}
{#snippet footer()}
<p style="margin: 0">This is footer.</p>
{/snippet}
<div>
<div style={wrapStyle}>
{#each switches as item (item.text)}
<span style={switchStyle}>
<span>{item.text}</span>
<Switch aria-label={item.text} checked={item.checked?.()} onChange={item.onChange} size="small" />
</span>
{/each}
<span style={switchStyle}>
<span>分页控件:</span>
<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>
</span>
</div>
<Table
bind:expandedRowKeys
{bordered}
{columns}
{dataSource}
{expandCellFixed}
expandedRow={expandable ? expandedRow : undefined}
footer={showFooter ? footer : undefined}
{loading}
{onChange}
pagination={tablePagination}
{resizable}
{rowSelection}
{scroll}
{showHeader}
title={showTitle ? 'This is title.' : undefined}
>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<span>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</span>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>
</div>完全自定义渲染
一般情况下,使用 cell snippet 即可,但是你也可以通过传递 Column.useFullRender=true 来开启完全自定义渲染模式,此时 snippet 的输出会替换整个单元格,复选框按钮、展开按钮、缩进等节点将会作为 snippet 传给 headerCell 与 cell,你可以进一步来定义表头和单元格的内容渲染方式。
其中 headerCell 的参数为:
{
column: ColumnProps,
title: string | undefined, // 列标题
filter: Snippet | undefined, // 筛选按钮
sorter: Snippet | undefined, // 排序按钮
selection: Snippet | undefined, // 选择按钮
}
cell 的参数中与完全自定义渲染有关的部分为:
{
expandIcon: Snippet | undefined, // 展开按钮
selection: Snippet | undefined, // 选择按钮
indentText: Snippet | undefined, // 缩进
}
使用 {@render selection?.()} 渲染这些节点。
下方的例子则是将复选框与内容渲染至同一单元格和表头中。
| Name | 所有者 | ||
|---|---|---|---|
| | 0 KB | 姜 姜鹏志 | 2024-05-06 |
| | 5 KB | 郝 郝宣 | 2024-05-11 |
| | 10 KB | 姜 姜鹏志 | 2024-05-16 |
| | 15 KB | 郝 郝宣 | 2024-05-21 |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 |
| | 25 KB | 郝 郝宣 | 2024-05-31 |
| | 30 KB | 姜 姜鹏志 | 2024-06-05 |
| | 35 KB | 郝 郝宣 | 2024-06-10 |
| | 40 KB | 姜 姜鹏志 | 2024-06-15 |
| | 45 KB | 郝 郝宣 | 2024-06-20 |
| | 50 KB | 姜 姜鹏志 | 2024-06-25 |
| | 55 KB | 郝 郝宣 | 2024-06-30 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps, type RowSelectionProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: 'Name',
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,
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
];
const getData = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
const pagination = { pageSize: 12 };
const rowSelection: RowSelectionProps = {
hidden: true,
fixed: 'left',
};
const onChange = (changeInfo: unknown) => console.log(changeInfo);
</script>
<Table {columns} {dataSource} {onChange} {pagination} {rowSelection}>
{#snippet headerCell({ column, title, sorter, filter, selection })}
{#if column.dataIndex === 'name'}
<span style="display: inline-flex; align-items: center; padding-inline-start: 20px">
{@render selection?.()}
<span style="margin-inline-start: 8px">{title}</span>
{@render sorter?.()}
{@render filter?.()}
</span>
{:else}
{title}
{/if}
{/snippet}
{#snippet cell({ column, text, record, expandIcon, selection, indentText })}
{#if column.dataIndex === 'name'}
<span style="display: inline-flex; align-items: center; justify-content: center">
{@render indentText?.()}
{@render expandIcon?.()}
{@render selection?.()}
<span style="margin-inline-start: 8px">
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</span>
</span>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
{#snippet expandedRow({ record })}
<article>{record.name}</article>
{/snippet}
</Table>表头合并
用户可以通过表头合并功能进行表头的分组,表头合并支持与固定列、虚拟化、数据分组、列伸缩等功能复合使用。Svelte 版只有配置式写法,没有 Table.Column。
合并表头配置式写法
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| | 0 KB | 姜 姜鹏志 | 2024-05-06 | ||
| | 5 KB | 郝 郝宣 | 2024-05-11 | ||
| | 10 KB | 姜 姜鹏志 | 2024-05-16 | ||
| | 15 KB | 郝 郝宣 | 2024-05-21 | ||
| | 20 KB | 姜 姜鹏志 | 2024-05-26 | ||
| | 25 KB | 郝 郝宣 | 2024-05-31 | ||
| | 30 KB | 姜 姜鹏志 | 2024-06-05 | ||
| | 35 KB | 郝 郝宣 | 2024-06-10 | ||
| | 40 KB | 姜 姜鹏志 | 2024-06-15 | ||
| | 45 KB | 郝 郝宣 | 2024-06-20 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { IconMore } from '@fibrae/semi-icons-svelte';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
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[] = [
{
title: '基本信息',
fixed: 'left',
children: [
{
title: '标题',
dataIndex: 'name',
width: 300,
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: 100,
fixed: true,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
],
},
{
title: '其他信息',
children: [
{
title: '所有者',
dataIndex: 'owner',
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
},
],
},
{
title: '更多',
fixed: 'right',
width: 100,
align: 'center',
dataIndex: 'operate',
},
];
const getData = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData(46);
const rowSelection = { fixed: true };
const scroll = { x: 1000, y: 400 };
const onChange = (changeInfo: unknown) => console.log(changeInfo);
</script>
<Table {columns} dataSource={data} {onChange} {rowSelection} {scroll}>
{#snippet cell({ column, text, record })}
{#if column.dataIndex === 'name'}
<span>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</span>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else if column.dataIndex === 'operate'}
<IconMore />
{:else}
{text}
{/if}
{/snippet}
{#snippet expandedRow({ record })}
<article>{record.name}</article>
{/snippet}
</Table>行列合并
- 表头除了通过
children写法进行合并外,可通过设置column.colSpan进行表头的列合并。 - 表格支持行/列合并,由
column.onCell返回单元格属性colspan或者rowspan(注意是小写),设值为 0 时,该单元格不会渲染,也不调用cell。
type OnCell = (record: RecordType, rowIndex: number) => TableCellAttrs;
// TableCellAttrs:td 支持的属性与监听器,colspan 与 rowspan 为小写
| 标题 | 大小 | 所有者 | |
|---|---|---|---|
| | |||
| | 5 KB | 郝 郝宣 | 2024-05-11 |
| 姜 姜鹏志 | |||
| | 15 KB | 郝 郝宣 | 2024-05-21 |
| | 20 KB | 姜 姜鹏志 | 2024-05-26 |
代码
<script lang="ts">
import { format } from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
const figmaIconUrl = '/assets/placeholder/app.svg';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
// onCell returns the native lowercase colspan and rowspan; 0 leaves out the cell another cell spans over
const columns: ColumnProps[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
onCell: (_record, index) => {
if (index === 0) {
return { colspan: 4 };
}
if (index === 1) {
return { rowspan: 2 };
}
if (index === 2) {
return { rowspan: 0 };
}
return {};
},
},
{
title: '大小',
dataIndex: 'size',
onCell: (_record, index) => {
if (index === 0) {
return { colspan: 0 };
}
if (index === 1) {
return { rowspan: 2 };
}
if (index === 2) {
return { rowspan: 0 };
}
return {};
},
},
{
title: '所有者',
dataIndex: 'owner',
onCell: (_record, index) => (index === 0 ? { colspan: 0 } : {}),
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
onCell: (_record, index) => {
if (index === 0) {
return { colspan: 0 };
}
if (index === 1) {
return { rowspan: 2 };
}
if (index === 2) {
return { rowspan: 0 };
}
return {};
},
},
];
const dataSource: Row[] = [];
for (let i = 0; i < 5; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
dataSource.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
</script>
<Table {columns} {dataSource} pagination={false}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'name'}
<div>
<Avatar alt="" shape="square" size="small" src={figmaIconUrl} style="margin-right: 12px" />
{text}
</div>
{:else if column.dataIndex === 'size'}
{text} KB
{:else if column.dataIndex === 'owner'}
<div>
<Avatar color={record.avatarBg} size="small" style="margin-right: 4px">
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
{:else if column.dataIndex === 'updateTime'}
{format(new Date(text), 'yyyy-MM-dd')}
{:else}
{text}
{/if}
{/snippet}
</Table>API 参考
Table
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| aria-describedby | 表格的 aria-describedby。 | string | null | - |
| aria-label | 表格的可访问名称;没有 aria-label 与 aria-labelledby 时,有 title 的表格用 title 命名。 | string | null | - |
| aria-labelledby | 表格的 aria-labelledby,优先于 title。 | string | null | - |
| bordered | 是否展示外边框和列边框。 | boolean | false |
| cell | 数据单元格的内容,对所有数据列调用,参数含 text、record、index、column、expandIcon、isHovering 等,用 column.dataIndex 或 column.key 区分列;不处理的列需要自己渲染 text。列设置 useFullRender 时 snippet 替换整个单元格,参数里的 selection、indentText、expandIcon 用于摆放选择框、缩进和展开按钮。 | TypeSnippet<[TableCellSnippetProps<R>]> | - |
| childrenRecordName | 树形表格中,数据项里表示子级数据的字段名。 | string | "children" |
| clickGroupedRowToExpand | 点击分组行时展开或收起该组。 | boolean | false |
| columns | 列配置,详见 Column。列只含数据,列内的节点用 Table 的 cell、headerCell、sortIcon、filterIcon、filterDropdown 等 snippet 渲染。 | TypeColumnProps<R>[] | - |
| components | 覆盖 Table 的组成元素,如 header 与 body 的 outer、wrapper、row、cell,值为标签名,该元素替换对应位置的默认标签。 | TableComponents | - |
| currentPage | 当前页码,可用 bind:currentPage 双向绑定,翻页时写回。与 pagination.currentPage 同时传入时以它为准;pagination 没有 currentPage 时,Table 仍自己对 dataSource 分页,父级改值会翻页;pagination 传了 currentPage 时是受控分页,Table 不对 dataSource 分页。 | number | - |
| dataSource | 数据。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。原地增删元素(push、splice)会重新排序和筛选;只修改记录字段时,单元格显示新值,但不重新排序和筛选。 | R[] | - |
| defaultExpandAllGroupRows | 默认是否展开所有分组行,动态加载数据时不生效。 | boolean | false |
| defaultExpandAllRows | 默认是否展开所有行,动态加载数据时不生效。 | boolean | false |
| direction | 文字方向,默认值取 ConfigProvider 的 direction,可在此单独配置。 | 'ltr' | 'rtl' | - |
| empty | 无数据时展示的内容,不提供时显示语言包的空文案,提供空 snippet 时不显示任何内容。 | Snippet | - |
| expandAllGroupRows | 是否展开所有分组行。 | boolean | false |
| expandAllRows | 是否展开所有行。 | boolean | false |
| expandCellFixed | 展开按钮所在列是否固定,取值与 Column 的 fixed 相同。 | ColumnFixed | false |
| expandedRow | 展开行的内容,参数含 record、index、expanded;传入后每个可展开的行都有展开按钮,输出为空时该行不渲染。 | TypeSnippet<[TableExpandedRowSnippetProps<R>]> | - |
| expandedRowKeys | 展开的行 key 数组,可用 bind:expandedRowKeys 双向绑定,包含已展开的分组 key;不传时行展开不受控。 | (string | number)[] | - |
| expandIcon | 传 false 隐藏展开按钮;传 snippet 替换展开按钮的图标,参数 expanded,自定义图标不带旋转动画。 | Typeboolean | Snippet<[TableExpandIconSnippetProps]> | - |
| expandRowByClick | 点击行时是否展开可展开行。 | boolean | false |
| filterDropdown | 筛选面板,用于 filterDropdown 为 true 的列,参数含 column、临时筛选值 tempFilteredValue 与 onTempFilteredValueChange、onConfirm、onClear、onClose、onReset,用 column 区分列。 | TypeSnippet<[TableFilterDropdownSnippetProps<R>]> | - |
| filterDropdownItem | 筛选菜单中的一项,参数含 text、value、checked、level、onChange 等;传入后替换所有筛选项。 | TypeSnippet<[TableFilterDropdownItemSnippetProps<R>]> | - |
| filterIcon | 筛选按钮的内容,参数含 filtered 与 column。 | TypeSnippet<[TableFilterIconSnippetProps<R>]> | - |
| footer | 表格尾部,可以是纯文本,或带参数 pageData(当前页数据)的 snippet。 | Typestring | Snippet<[TableTitleSnippetProps<R>]> | - |
| groupBy | 分组依据,可以是数据项中的字段名,或返回字符串、数字的函数。 | TypeTableGroupBy<R> | - |
| groupSection | 分组行的内容,参数含 groupKey 与 group(该组的行 key)。 | TypeSnippet<[TableGroupSectionSnippetProps]> | - |
| groupSectionColumn | 分组行唯一一列的列配置,返回值不含 title、children、dataIndex。 | Type(groupKey: string | number, group: (string | number)[]) => Omit<ColumnProps<R>, 'title' | 'children' | 'dataIndex'> | - |
| headerCell | 列标题的内容,参数含 column、title 与 sorter、filter、selection;不处理的列需要自己渲染 title。列设置 useFullRender 时 snippet 替换整个表头单元格,sorter、filter、selection 用于摆放排序、筛选与选择控件。 | TypeSnippet<[TableHeaderCellSnippetProps<R>]> | - |
| headerStyle | 所有表头单元格(包括固定表头)的内联样式字符串。 | string | - |
| hideExpandedColumn | 表格可展开时,展开按钮默认与第一列内容渲染在同一个单元格;设为 false 时展开按钮单独占一列。 | boolean | true |
| indentSize | 树形数据每一层的缩进宽度,单位为像素。 | number | 20 |
| keepDOM | 折叠行时是否保留被折叠的 DOM。 | boolean | false |
| loading | 是否显示加载状态。 | boolean | false |
| onChange | 分页、排序、筛选变化时调用,参数含 pagination、filters、sorter 与 extra.changeType。 | Type(changeInfo: TableChangeInfo<R>) => void | - |
| onExpand | 点击展开按钮时调用,参数为展开状态、行数据(分组行为 groupKey 对象)和鼠标事件。 | Type(expanded: boolean, record: TableIncludeGroupRecord<R>, event: MouseEvent) => void | - |
| onExpandedRowsChange | 展开的行变化时调用,参数为展开的行数据数组。 | Type(expandedRows: TableIncludeGroupRecord<R>[]) => void | - |
| onGroupedRow | 类似 onRow,单独用于分组行,返回值是 tr 的属性与小写监听器。index 在未开启 virtualized 时是分组序号,开启后是该行在虚拟列表中的位置。 | Type(record: { groupKey: string | number; records: R[] }, index: number) => TableRowAttrs | - |
| onHeaderRow | 设置表头行的属性与小写监听器,返回值会合并到表头行(不含 style)。 | Type(columns: ColumnProps<R>[], index: number) => Omit<TableRowAttrs, 'style'> | - |
| onRow | 设置数据行的属性与小写监听器(如 onclick、onmouseenter),第三个参数为行状态。返回的监听器在行自身的处理和 onRowXxx 回调之后调用。 | Type(record: R, index: number, rowState: { disabled: boolean; selected: boolean }) => TableRowAttrs | - |
| onRowContextMenu | 数据行触发右键菜单时调用,先于 onRow 返回的 oncontextmenu 调用。 | Type(record: R, event: MouseEvent) => void | - |
| onRowDoubleClick | 双击数据行时调用,先于 onRow 返回的 ondblclick 调用。 | Type(record: R, event: MouseEvent) => void | - |
| onRowMouseEnter | 鼠标进入数据行时调用,先于 onRow 返回的 onmouseenter 调用。 | Type(record: R, event: MouseEvent) => void | - |
| onRowMouseLeave | 鼠标离开数据行时调用,先于 onRow 返回的 onmouseleave 调用。 | Type(record: R, event: MouseEvent) => void | - |
| onScroll | 虚拟化表体的纵向偏移变化时调用,参数为 scrollOffset,横向滚动不触发。 | Type(args: TableVirtualizedScrollArgs) => void | - |
| pagination | 分页配置,传 false 关闭分页。 | boolean | TablePaginationProps | true |
| paginationContent | 替换分页信息与分页器,参数 pagination 可直接展开给 Pagination;与 pagination 属性同名空间,所以单独命名。 | TypeSnippet<[TablePaginationSnippetProps]> | - |
| prefixCls | 样式名前缀。 | string | "semi-table" |
| resizable | 是否开启列宽伸缩,需要伸缩的列必须提供 width。传入对象时可配置 onResize、onResizeStart、onResizeStop 与 handlerClassName。 | Typeboolean | ResizableProps<R> | false |
| rowExpandable | 判断某一行是否可展开,返回 false 时不渲染该行的展开按钮。 | TypeTableRowExpandable<R> | - |
| rowKey | 行 key 的取值:数据项的字段名,或返回字符串、数字的函数。 | TypeTableRowKey<R> | "key" |
| rowSelection | 行选择配置,传 true 使用默认配置,详见 rowSelection。 | Typeboolean | RowSelectionProps<R> | - |
| rowSpanHover | 悬停在带 rowspan 的单元格上时,高亮它跨越的所有行。 | boolean | false |
| scroll | 滚动区域配置,x 为横向宽度,y 为纵向高度,详见 scroll。 | TableScroll | - |
| selectedRowKeys | 选中行的 key 数组,可用 bind:selectedRowKeys 双向绑定,勾选时写回。与 rowSelection.selectedRowKeys 同时传入时以它为准;需要同时传 rowSelection 才显示选择列。 | (string | number)[] | - |
| selectionCell | 选择列的单元格,表头与数据行都调用,参数含 selected、inHeader、originNode(默认复选框)等,用 originNode 保留默认复选框。 | TypeSnippet<[TableSelectionCellSnippetProps<R>]> | - |
| showHeader | 是否显示表头。 | boolean | true |
| size | 表格尺寸,影响单元格的内边距。 | TableSize | "default" |
| sortIcon | 排序图标,参数含 sortOrder 与 column。 | TypeSnippet<[TableSortIconSnippetProps<R>]> | - |
| sticky | 将表头固定在页面顶部,传对象时 top 为距离滚动容器顶部的距离。 | TableSticky | - |
| title | 表格标题,可以是纯文本,或带参数 pageData(当前页数据)的 snippet。 | Typestring | Snippet<[TableTitleSnippetProps<R>]> | - |
| virtualized | 开启虚拟化,需同时传 scroll.y(数字)。对象形式可配置行高 itemSize 与额外渲染的行数 overscanCount。 | TableVirtualized | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| getCurrentPageData | 返回当前页数据的深拷贝,含 dataSource 与 groups。 | Type() => Pick<BasePageData<AnyRecord>, "dataSource" | "groups"> |
| scrollTo | 虚拟化表格滚动到指定纵向偏移,参数为像素。 | Type(offset: number) => void |
| scrollToItem | 虚拟化表格滚动到指定行,第二个参数为对齐方式。 | Type(index: number, align?: Parameters<TableHandle['scrollToItem']>[1]) => void |
class、style(字符串)、id、data-* 等原生属性落在最外层元素;aria-label、aria-labelledby、aria-describedby 落在 role="table" 的元素上。有标题而使用方没有传名称时,表格通过 aria-labelledby 指向标题。
TableProps、ColumnProps、TableHandle 与各 snippet 的参数类型都可以从 @fibrae/semi-ui-svelte 导入,它们的泛型参数 R(下文的 RecordType)默认为 Record<string, any>。
virtualized 传对象时的字段(TableVirtualizedProps):
| 字段 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| itemSize | 每行的高度,不传时按 size 取默认值:default 为 53,middle 为 45,small 为 37,组头行同普通行 | number | ((index: number, row: { sectionRow?: boolean, expandedRow?: boolean }) => number) | 53 |
| overscanCount | 可视区上下额外渲染的行数 | number | 2 |
components 的值是标签名,该标签替换对应位置的默认标签。可以替换的位置(TableComponents):
| 位置 | 说明 |
|---|---|
| header.outer、header.wrapper、header.row、header.cell | 表头的 table、thead、tr、th |
| body.outer、body.wrapper、body.row、body.cell | 表体的 table、tbody、tr、td |
| body.colgroup.wrapper、body.colgroup.col | colgroup 与 col |
headerStyle 示例
headerStyle 会应用到所有表头 <th> 元素,包括 fixed 表头。
<script lang="ts">
import { Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
const columns: ColumnProps[] = [
{ 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',
}));
</script>
<Table {columns} {dataSource} scroll={{ x: 900 }} headerStyle="background-color: #F5F6F7; font-weight: 600" />
headerStyle 是 CSS 字符串。
Svelte 版的 Table 不是泛型组件,传给它的 columns、rowSelection、onRow 等都按 Record<string, any> 取记录。记录有具体类型时,把 dataSource 声明成该类型,在 snippet 和回调里用 as 收窄:
<script lang="ts">
import { Table, type ColumnProps } from '@fibrae/semi-ui-svelte';
interface Row {
key: string;
name: string;
age: number;
address: string;
}
const columns: ColumnProps[] = [
{ title: 'Name', dataIndex: 'name', width: 200 },
// ...
];
const data: Row[] = [
{ key: '1', name: 'John Brown', age: 32, address: 'New York No. 1 Lake Park' },
// ...
];
</script>
<Table {columns} dataSource={data}>
{#snippet cell({ column, record, text })}
{#if column.dataIndex === 'age'}
{(record as Row).age} 岁
{:else}
{text}
{/if}
{/snippet}
</Table>
分页配置 TablePaginationProps 在 Pagination 的属性上增加 position 与 formatPageText,onChange 的参数是 (currentPage, pageSize)。
onHeaderRow / onRow 用法
onHeaderRow 中可以返回 th 支持的属性或者事件 onRow 中可以返回 tr 支持的属性或者事件
onRow 的第三个参数 rowStatus 可以获取当前行的状态信息,包括 disabled 和 selected 属性。这在需要根据行的选中或禁用状态执行不同逻辑时非常有用,例如点击行时判断是否允许选中。
返回对象里的事件监听是小写的原生名(onclick、onmouseenter、onmouseleave),样式类用 class,style 是字符串。Table 自己的处理、onRowMouseEnter 等回调先于这里返回的监听器调用。
<script lang="ts">
import { Table, type TableProps } from '@fibrae/semi-ui-svelte';
const onRow: TableProps['onRow'] = (record, index, rowStatus) => ({
onclick: () => {
// rowStatus.disabled 表示当前行是否被禁用
// rowStatus.selected 表示当前行是否被选中
if (rowStatus.disabled) {
return;
}
console.log('点击行', index, rowStatus);
},
onmouseenter: () => {}, // 鼠标移入行
onmouseleave: () => {}, // 鼠标移出行
class: '',
// 其他可以作用于 tr 的属性或事件
});
const onHeaderRow: TableProps['onHeaderRow'] = (columns, index) => ({
onclick: () => {}, // 点击表头行
class: '',
// 其他可以作用于 tr 的属性或事件
});
</script>
<Table {columns} {dataSource} {onRow} {onHeaderRow} />
Column
列配置只接受下表的属性,拼错的属性名会报类型错误。需要在列上携带自定义字段时,用交叉类型扩展,如 ColumnProps<RecordType> & { tooltip: string }。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | 设置列的对齐方式,在 RTL 时会自动切换 | 'left' | 'right' | 'center' | 'left' |
| class | 列样式名 | string | |
| children | 表头合并时用于子列的设置 | Column[] | |
| colSpan | 表头列合并,设置为 0 时,不渲染 | number | |
| dataIndex | 列数据在数据项中对应的 key,使用排序或筛选时必传,且需要保持不重复 | string | |
| defaultFilteredValue | 筛选的默认值,值为已筛选的 value 数组 | any[] | |
| defaultSortOrder | 排序的默认值,可设置为 'ascend'|'descend'|false | boolean | string | false |
| ellipsis | 文本缩略,开启后 table-layout 会自动切换为 fixed | boolean | { showTitle: boolean } | false |
| filterChildrenRecord | 是否需要对子级数据进行本地过滤,开启该功能后如果子级符合过滤标准,父级即使不符合仍然会保留 | boolean | |
| filterDropdown | 为 true 时显示筛选按钮,面板内容由 Table 的 filterDropdown snippet 提供,需要自行编写各种交互 | boolean | |
| filterDropdownProps | 透传给 Dropdown 的属性(不含 visible、trigger、onVisibleChange 等),触发方式固定为点击,详情点击Dropdown API | object | |
| filterDropdownVisible | 控制 Dropdown 的 visible,详情点击Dropdown API | boolean | |
| filterMultiple | 是否多选 | boolean | true |
| filterConfirmMode | 筛选确认模式。immediate 为点击筛选项立即生效;confirm 为点击筛选项后需点击确定按钮才生效,此时下拉面板底部会显示确定和重置按钮 | 'immediate' | 'confirm' | 'immediate' |
| filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | any[] | |
| filters | 表头的筛选菜单项。 | Filter[] | |
| fixed | 列是否固定,可选 true(等效于 left) 'left' 'right',在 RTL 时会自动切换 | boolean | string | false |
| key | 列的标识,用于选择、排序、筛选与列宽;已设置唯一的 dataIndex 时可以省略 | string | number | |
| resize | 是否开启 resize 模式,只有 Table resizable 开启后此属性才会生效 | boolean | |
| showSortTip | 是否展示排序提示,如果设置了 sortOrder,排序受控,则该参数不会生效 | boolean | false |
| sortChildrenRecord | 是否对子级数据进行本地排序 | boolean | |
| sortOrder | 排序的受控属性,外界可用此控制列的排序,可设置为 'ascend'|'descend'|false | boolean | string | false |
| sorter | 排序函数,本地排序使用一个函数 (参考 Array.sort 的 compareFunction),需要服务端排序可设为 true。必须给排序列设置一个独立的 dataIndex,必须为 dataSource 里面的每条数据项设置独立的 key | boolean | (r1: RecordType, r2: RecordType, sortOrder: 'ascend' | 'descend') => number | true |
| title | 列头显示的纯文本,同时是省略时的 Tooltip 内容和排序按钮的名称。需要自定义内容时用 Table 的 headerCell snippet | string | |
| useFullRender | 是否完全自定义渲染,用法详见完全自定义渲染,开启此功能会造成一定的性能损耗 | boolean | false |
| width | 列宽度 | string | number | |
| onCell | 设置单元格属性,返回的 colspan、rowspan 为小写 | (record: RecordType, rowIndex: number) => object | |
| onFilter | 本地模式下,确定筛选的运行函数。必须给筛选列设置一个独立的 dataIndex,必须为 dataSource 里面的每条数据项设置独立的 key | (filteredValue: any, record: RecordType) => boolean | |
| onFilterDropdownVisibleChange | 自定义筛选菜单可见变化时回调 | (visible: boolean) => void | |
| onHeaderCell | 设置头部单元格属性,index 为表头行的序号 | (column: Column, columnIndex: number, index: number) => object |
Column 只是数据,没有 Table.Column 组件,也没有 render、renderFilterDropdown、renderFilterDropdownItem、shouldCellUpdate。单元格、表头、排序与筛选图标和筛选项的自定义渲染用 Table 的 snippet:cell、headerCell、sortIcon、filterIcon、filterDropdown、filterDropdownItem,参数里的 column 用来区分列,参数见上文 Table 的属性表。
一些上面用到的类型定义:
type Filter = {
value: any;
text: string;
children?: Filter[];
};
Column.onCell / onHeaderCell 用法
与 onRow、onHeaderRow类似,在 column.onCell column.onHeaderCell 中也能返回 td/th 支持的属性或事件
rowSelection
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| class | 所处列样式名 | string | |
| checkRelation | 父子节点选择关联模式。设置为 'related' 时,选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选);默认为 'unRelated' 即父子节点选择互不影响 | 'related' | 'unRelated' | 'unRelated' |
| clickRow | 是否启用点击行选择功能。开启后,点击行任意位置(包括固定列)都会触发行选择/取消选择。被禁用的行(通过 getCheckboxProps 设置)无法通过点击选中;disabled 为 true 时点击行也不会选中 | boolean | false |
| defaultSelectedRowKeys | 初始选中项的 key 数组,只在挂载时读取,传了 selectedRowKeys 时不生效 | (string | number)[] | |
| disabled | 表头的 Checkbox 是否禁用;为 true 时 clickRow 点击行选择也不生效 | boolean | false |
| fixed | 把选择框列固定在左边 | boolean | false |
| getCheckboxProps | 选择框的默认属性配置 | (record: RecordType) => object | |
| hidden | 是否隐藏选择列 | boolean | false |
| selectedRowKeys | 指定选中项的 key 数组,需要和 onChange 进行配合 | (string | number)[] | |
| width | 自定义列表选择框宽度 | string | number | |
| onChange | 选中项发生变化时的回调。第一个参数会保存上次选中的 row keys,即使你做了分页受控或更新了 dataSource FAQ | (selectedRowKeys: (string | number)[], selectedRows: RecordType[]) => void | |
| onCell | 设置选择列数据单元格的属性 | (record: RecordType, rowIndex: number) => object | |
| onHeaderCell | 设置头部单元格属性,index 为表头行的序号 | (column: Column, columnIndex: number, index: number) => object | |
| onSelect | 用户手动选择某行的回调。event 为选择框的 change 事件;clickRow 点击行时为鼠标事件 | (record: RecordType, selected: boolean, selectedRows: RecordType[], event: Event) => void | |
| onSelectAll | 用户手动点击表头选择框的回调,会选中/取消选中当前筛选结果里的所有可选行(不含禁用行),changedRows 只包含选中状态发生变化的行 | (selected: boolean, selectedRows: RecordType[], changedRows: RecordType[], event: Event) => void |
自定义选择框用 Table 的 selectionCell snippet,不再有 renderCell 和 shouldCellUpdate。
scroll
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| scrollToFirstRowOnChange | 当分页、排序、筛选变化后是否自动滚动到表格顶部。当设置了 scroll.y 时,会重置表格内部滚动位置到顶部;当未设置 scroll.y 时,会滚动页面到表格头部位置 | boolean | false |
| x | 设置横向滚动区域的宽,可以为像素值、百分比或 'max-content' | string|number | |
| y | 设置纵向滚动区域的高,可以为像素值 | number |
pagination
翻页组件配置。pagination 建议不要使用字面量写法。
注意:pagination.onChange 设置后,Table onChange 不再响应分页器变化。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| currentPage | 当前页码,传入后分页受控 | number | - |
| formatPageText | 翻页区域文案自定义格式化,传 false 关闭文案显示;该项影响表格翻页区域左侧文案显示,不同于 Pagination 组件的 showTotal 参数,请注意甄别。 | boolean | ({ currentStart: number, currentEnd: number, total: number }) => string | true |
| pageSize | 每页条数 | number | 10 |
| position | 位置 | 'bottom' | 'top' | 'both' | 'bottom' |
| total | 数据总数 | number | 0 |
| preventPageChangeOnPageSizeChange | 切换 pageSize 时是否阻止自动调整 currentPage。默认情况下,切换 pageSize 时组件会自动计算新的 currentPage 以保持当前数据位置,设为 true 后由用户自行控制页码变化 | boolean | false |
| onChange | 页码或每页条数变化时的回调,设置后 Table 的 onChange 不再响应分页器变化 | (currentPage: number, pageSize: number) => void | |
| onPageChange | 页码变化时的回调 | (currentPage: number) => void | |
| onPageSizeChange | 每页条数变化时的回调 | (pageSize: number) => void |
其他配置详见Pagination
Resizable
resizable 对象型的参数,主要包括一些表格列伸缩时的事件方法。这些事件方法都可以返回一个对象,该对象会和最终的 column 合并。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| handlerClassName | 列宽调整期间加到该列上的 class 名 | string | 'resizing' |
| onResize | 表格列改变宽度时触发 | (column: Column) => Column | void | |
| onResizeStart | 表格列开始改变宽度时触发 | (column: Column) => Column | void | |
| onResizeStop | 表格列停止改变宽度时触发 | (column: Column) => Column | void |
方法
通过 bind:this 可以访问到 Table 暴露的方法,类型为 TableHandle:
<script lang="ts">
import { onMount } from 'svelte';
import { Table, type TableHandle } from '@fibrae/semi-ui-svelte';
let table = $state<TableHandle>();
onMount(() => {
table?.getCurrentPageData(); // => { dataSource: [/*...*/], groups: /*...*/ }
});
</script>
<Table bind:this={table} {columns} {dataSource} />
| 名称 | 描述 |
|---|---|
| getCurrentPageData() | 返回当前页的数据对象:{ dataSource: RecordType[], groups: Map<string | number, Set<string | number>> },groups 的键是分组键,值是该组的行键集合 |
| scrollTo(offset) | 虚拟化表格滚动到指定的像素偏移 |
| scrollToItem(index, align?) | 虚拟化表格滚动到指定的行,align 为 'auto' | 'smart' | 'center' | 'start' | 'end' |
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;名称为「筛选此列数据」,有筛选值时为「筛选此列数据,筛选已生效」。filterIconsnippet 只替换按钮内的图标,按钮的名称和键盘操作不变
- 固定表头(
scroll.y、sticky)或虚拟化时,表头和表体渲染为两个 table 元素,二者的共同容器是 role 为 table 的元素,内部 table 为 role none,单元格仍与列头关联 - 虚拟化表格的
aria-rowcount为表头行数加数据总行数,数据行的aria-rowindex接在表头行之后,屏幕阅读器据此报出不在 DOM 中的行
已知限制:
- 带排序的列,
title渲染在排序按钮里,title中不要放按钮、链接等可交互内容
RTL/LTR
- Table 的 RTL 默认值为 ConfigProvider direction,可以通过 Table direction 覆盖
- Table 列的 align 与 fixed 属性会在 RTL 时会自动切换,left <-> right
- Table 的树形数据暂不支持 RTL(Chrome、Safari 浏览器表现与 Firefox 表现不同)
文案规范
- 表格标题
- 表格标题应清晰的让用户感知到表格的目的;
- 为复杂表格添加描述,为用户提供更多关于表格的上下文信息;
- 使用句子大小写;
- 列标题
- 保持列标题简洁,建议使用 1-2 个词作为列标题;
- 当列标题较长时,建议 2 行显示,剩余文字缩略并在 Tooltip 中显示完全;
- 采用 Sentence case 的大小写规则;
- 列标题使用句子大小写;
- 表格操作
- 可以遵循 Button 的文案规范
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration-table_body-bg | var(--semi-transition_duration-none) | 表格-背景颜色-动画持续时间 |
| $transition_function-table_body-bg | var(--semi-transition_function-easeOut) | 表格-背景颜色-过渡曲线 |
| $transition_delay-table_body-bg | 0ms | 表格-背景颜色-延迟时间 |
| $transition_duration-table_row-head-bg | var(--semi-transition_duration-none) | 表格-行头-背景颜色-动画持续时间 |
| $transition_function-table_row-head-bg | linear | 表格-行头-背景颜色-过渡曲线 |
| $spacing-table-paddingY | $spacing-base | 表格空数据占位及虚拟列表分组垂直内边距 |
| $spacing-table-paddingX | $spacing-base-tight | 表格空数据占位及虚拟列表分组水平内边距 |
| $spacing-table_middle-paddingY | $spacing-base-tight | 中尺寸表格单元格垂直内边距 |
| $spacing-table_small-paddingY | $spacing-tight | 小尺寸表格单元格垂直内边距 |
| $spacing-table_title-paddingY | $spacing-base | 表格标题垂直内边距 |
| $spacing-table_title-paddingX | 0 | 表格标题水平内边距 |
| $spacing-table_footer-padding | $spacing-base | 表格 footer 内边距 |
| $spacing-table_expand_row-paddingLeft | 16px | 表格展开行左侧内边距 |
| $spacing-table_expand_row-paddingRight | 16px | 表格展开行右侧内边距 |
| $spacing-table_resizable-offset-y | 4px | 可拖动拉伸操作垂直偏移量 |
| $spacing-table_resizable-bottom | 4px | 可拖动拉伸操作底部距离 |
| $spacing-table_row_head-paddingY | $spacing-base-tight | 表头垂直内边距 |
| $spacing-table_row_head-paddingX | $spacing-base | 表头水平内边距 |
| $spacing-table_react_resizable_handle-right | -1px | 可拖动拉伸操作向右偏移量 |
| $spacing-table_tbody_rowCell-padding | $spacing-base | 表格体单元格内边距(默认尺寸) |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingX | 16px | 表格选择状态行(非复选框列)单元格水平内边距 |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingY | 10px | 表格选择状态行(非复选框列)垂直内边距 |
| $spacing-table_column_sorter-marginLeft | $spacing-extra-tight | 表头标题与排序按钮间距 |
| $spacing-table_column_filter-marginLeft | $spacing-extra-tight | 列过滤器按钮左侧外边距 |
| $spacing-table_bordered_titler-paddingLeft | $spacing-base | 带边框表格标题左侧内边距 |
| $spacing-table_bordered_titler-paddingRight | $spacing-base | 带边框表格标题右侧内边距 |
| $spacing-table_expand_icon-marginRight | 8px | 行展开按钮右侧外边距 |
| $spacing-table_panel_operation-paddingX | $spacing-base | 表格操作栏水平内边距 |
| $spacing-table_panel_operation-paddingY | $spacing-tight | 表格操作栏垂直内边距 |
| $width-table_base_border | 1px | 表格单元格分割线宽度 |
| $width-table_header_border | 1px | 表格表头分割线宽度 |
| $width-table_resizer_border | 2px | 表格拉伸列标示线宽度 |
| $width-table_column_selection | 48px | 表格选择列及展开列宽度 |
| $width-table_column_sorter-icon | 16px | 表格排序按钮宽度 |
| $height-table_column_sorter-icon | 16px | 表格排序按钮高度 |
| $width-table_column_filter-icon | 16px | 表格过滤按钮宽度 |
| $height-table_column_filter-icon | 16px | 表格过滤按钮高度 |
| $width-table_cell_fixed_left_last | 1px | 表格左侧固定列右侧分割线宽度 |
| $width-table_cell_fixed_right_first | 1px | 表格右侧固定列左侧分割线宽度 |
| $width-table_react_resizable_handle | 9px | 表格伸缩列调节热区宽度 |
| $height-table_pagination_outer_min | 60px | 表格分页器高度 |
| $height-table_column_filter_dropdown | 290px | 表格过滤筛选列表最大高度 |
| $color-table_panel-bg-default | var(--semi-color-primary) | 操作区域样式默认背景颜色 |
| $color-table_panel-text-default | var(--semi-color-primary-light-active) | 表格操作栏选中项文本颜色 |
| $color-table-bg-default | var(--semi-color-bg-1) | 表格固定列及吸顶表头背景颜色 |
| $color-table-text-default | var(--semi-color-text-0) | 单元格默认文字颜色 |
| $color-table-border-default | var(--semi-color-border) | 表格描边颜色 |
| $color-table_shadow-bg-default | rgb(from var(--semi-black) r g b / 0.08) | 表格滚动后阴影颜色 |
| $color-table_shadow-bg-dark | rgb(from var(--semi-black) r g b / 0.4) | 表格滚动后阴影颜色 - 暗色模式 |
| $color-table_shadow-border-default | var(--semi-color-border) | 表格拟阴影 描边颜色 |
| $color-table_th-bg-default | var(--semi-color-bg-1) | 表头背景底色,须不透明以遮住滚动到固定列和吸顶表头下的内容 |
| $color-table_th-fill-default | var(--semi-color-fill-0) | 表头背景叠加的填充色 |
| $color-table_th-border-default | var(--semi-color-border) | 表头底部分割线颜色 |
| $color-table_th-text-default | var(--semi-color-text-2) | 表头文字颜色 |
| $color-table_th-clickSort-bg-hover | var(--semi-color-fill-1) | 点击表头触发排序背景色 - 悬浮 |
| $color-table_pl-bg-default | transparent | |
| $color-table_body-bg-default | var(--semi-color-bg-1) | 表格行背景颜色 - 默认态 |
| $color-table_body-bg-hover | var(--semi-color-fill-0) | 表格行背景颜色 - 悬浮态 |
| $color-table_footer-bg-default | var(--semi-color-fill-0) | 表格 footer 背景颜色 - 默认 |
| $color-table_row_expanded-bg-default | var(--semi-color-fill-0) | 表格展开行背景颜色 - 默认 |
| $color-table_expanded_icon-default | var(--semi-color-text-2) | 表格展开行图标颜色 - 默认 |
| $color-table_expanded-bg-default | transparent | 表格展开行图标容器背景颜色 - 默认 |
| $color-table_filter_on-text-default | var(--semi-color-primary) | 表格过滤按钮颜色 - 激活态 |
| $color-table_filter-text-default | var(--semi-color-text-2) | 表格过滤按钮颜色 - 默认态 |
| $color-table_sorter_on-text-default | var(--semi-color-primary) | 表格排序按钮颜色 - 激活态 |
| $color-table_sorter-text-default | var(--semi-color-text-2) | 表格排序按钮颜色 - 默认态 |
| $color-table_sorter-text-hover | var(--semi-color-text-2) | 表格排序按钮颜色 - 悬浮态 |
| $color-table_page-text-default | var(--semi-color-text-2) | 表格翻页器文本颜色 |
| $color-table_resizer-bg-default | var(--semi-color-primary) | 表格拉伸标示线颜色 |
| $color-table_selection-bg-default | var(--semi-grey-0) | 表格分组背景色 |
| $color-table_placeholder-text-default | var(--semi-color-text-2) | 表格空数据文本颜色 |
| $color-table_cell-bg-hover | var(--semi-color-bg-0) | 让表格在 hover 时正确显示 $color-table_body-bg-hover 颜色,如无必要不要修改 |
| $font-table_base-fontSize | 14px | 表格默认文本字号 |
| $border-table_base-borderStyle | solid | 表格描边样式 |
| $width-table_fixed_shadow | 6px | 表格固定列滚动阴影宽度 |
| $z-table_fixed_column | $z-table-fixed | fixed列的zIndex值 |
FAQ
-
点击第二页的行选择按钮,会跳转到第一页?
Table 的 dataSource 更新后,会将页码重置到初始态。请检查数据源是否在组件渲染时发生了变化。
Svelte 的 Table 逐项比较新旧
dataSource,每一项都是原来的记录时不会重置,新增或删除记录会重置。dataSource用$state.raw保存并在需要时整体替换,不要在每次求值时重新生成记录对象。<script lang="ts"> let dataSource = $state.raw<Record<string, any>[]>([]); // 只在需要时整体替换 const load = async () => { dataSource = await fetchData(); }; </script> <!-- 错误:每次求值都会生成新的记录对象 --> <Table dataSource={list.map((item) => ({ ...item }))} {columns} /> -
筛选后的数据条数不对?
请检查你的筛选列和数据源是否配置正确。
筛选列需设置独立的 dataIndex,同时 dataSource 需要设置独立的 key,请参考 dataSource API。否则筛选功能无法正常工作。
-
表格数据为何没有更新?
Svelte 通过 rune 追踪变化:
dataSource用$state或$state.raw保存,替换整个数组或调用push、splice等方法都会触发更新;$state.raw下直接修改某条记录的字段不会触发,需要整体替换数组。columns同理,依赖响应式数据时用$derived,不依赖时定义在<script>顶层。 -
为何我的表格行不能选中以及展开?
请指定 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 暂不支持单行选择功能,用户可以通过自定义方式实现单选。