如何使用
往 Table 传入表头 columns 和数据 dataSource 进行渲染。
<script setup lang="ts">
import { Table } from '@fibrous/semi-ui-vue';
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>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false" />
</template>
代码演示
基本表格
对于表格,最基本的两个参数为 dataSource 和 columns,前者为数据项,后者为每列的配置,二者皆为数组类型。
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| 2M | 已交付 | 姜 姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 已延期 | 郝 郝宣 | 2020-01-17 05:31 | ||
| 34KB | 待评审 | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script setup lang="ts">
import { IconClear, IconComment, IconMore, IconTickCircle } from '@fibrous/semi-icons-vue';
import { Avatar, Table, Tag, type ColumnProps } from '@fibrous/semi-ui-vue';
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<Row>[] = [
{
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>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<Tag v-else-if="column.dataIndex === 'status'" :color="tagConfig[text as Row['status']].color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="tagConfig[text as Row['status']].icon" />
</template>
{{ tagConfig[text as Row['status']].text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>Table.Column 写法
你也可以把 Table.Column 写成 Table 的子组件来定义列:列的数据写在属性上,单元格和标题的自定义内容写在它的 #cell、#title 等插槽中。Table 只读取直接子项,你不能够使用任何组件包裹 Table.Column,v-if、v-for 展开后的直接子项可以。
| 标题 | 大小 | 所有者 | 更新时间 | |
|---|---|---|---|---|
| 2M | 姜 姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝 郝宣 | 2020-01-17 05:31 | ||
| 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, TableColumn as Column, type AvatarProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: string;
updateTime: string;
avatarBg: AvatarProps['color'];
}
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>
<template>
<Table :dataSource="data" :pagination="false">
<Column key="name" dataIndex="name" title="标题">
<template #cell="{ text, record }">
<div>
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
</template>
</Column>
<Column key="size" dataIndex="size" title="大小" />
<Column key="owner" dataIndex="owner" title="所有者">
<template #cell="{ text, record }">
<div>
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
</template>
</Column>
<Column key="updateTime" dataIndex="updateTime" title="更新时间" />
<Column key="operate" dataIndex="operate" title="">
<template #cell>
<IconMore />
</template>
</Column>
</Table>
</template>行选择操作
往 Table 传入 rowSelection 即可打开此功能。
- 点击表头的选择框,会选择
dataSource里所有不是disabled状态的行。选择所有行回调函数为onSelectAll; - 点击行的选择框会选中当前行。它的回调函数为
onSelect;
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|---|
| 2M | 已交付 | 姜 姜鹏志 | 2020-02-02 05:13 | |||
| 2M | 已延期 | 郝 郝宣 | 2020-01-17 05:31 | |||
| 34KB | 待评审 | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script setup lang="ts">
import { IconClear, IconComment, IconMore, IconTickCircle } from '@fibrous/semi-icons-vue';
import { Avatar, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
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<Row>[] = [
{
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<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
const pagination = { pageSize: 3 };
</script>
<template>
<Table :columns="columns" :dataSource="data" :pagination="pagination" :rowSelection="rowSelection">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }}</template>
<Tag v-else-if="column.dataIndex === 'status'" :color="tagConfig[text as Row['status']].color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="tagConfig[text as Row['status']].icon" />
</template>
{{ tagConfig[text as Row['status']].text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">{{ text }}</template>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>自定义渲染
用户可以使用 #cell 作用域插槽来自定义单元格的渲染,该功能适用于需要渲染较为复杂的单元格内容时。Table 上的 #cell 对所有数据列调用,用 column.dataIndex 或 column.key 区分列;插槽输出为空时显示原始值。只想定制一列时,也可以把 #cell 写在该列的 Table.Column 上,它优先于 Table 上的插槽。
#cell 的参数是一个对象,包含以下属性:
text:column.dataIndex对应的值,没有dataIndex的列为数据项本身record: 当前行的数据项index: 数据项在同级中的序号column: 当前列的配置expandIcon: 展开图标(当使用树形数据或可展开行时)isHovering: 当前行是否处于悬停状态selection、indentText: 选择框与缩进内容,仅在列开启useFullRender时提供,见完全自定义渲染
通过 isHovering 参数,可以实现鼠标悬停时显示操作按钮等交互效果。
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| 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 setup lang="ts">
import { shallowRef } from 'vue';
import { IconClear, IconComment, IconDelete, IconTickCircle } from '@fibrous/semi-icons-vue';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrous/semi-illustrations-vue';
import { Avatar, Button, Empty, Table, Tag, Typography, type ColumnProps } from '@fibrous/semi-ui-vue';
const { Text } = Typography;
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue' | 'green';
}
const 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',
},
];
const dataSource = shallowRef(raw);
const removeRecord = (key: string) => {
dataSource.value = dataSource.value.filter((data) => data.key !== key);
};
const resetData = () => {
dataSource.value = [...raw];
};
const columns: ColumnProps<Row>[] = [
{
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>
<template>
<div>
<Button :style="{ marginBottom: '10px' }" @click="resetData">重置</Button>
<Table :columns="columns" :dataSource="dataSource" :pagination="false" :style="{ minHeight: '350px' }">
<template #cell="{ column, record, text }">
<span v-if="column.dataIndex === 'name'" :style="{ display: 'flex', alignItems: 'center' }">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
<!-- Width = the cell's 400px minus the non-text content -->
<Text :ellipsis="{ showTooltip: true }" :style="{ width: 'calc(400px - 76px)' }">{{ text }}</Text>
</span>
<Tag v-else-if="column.dataIndex === 'status'" :color="tagConfig[text as Row['status']].color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="tagConfig[text as Row['status']].icon" />
</template>
{{ tagConfig[text as Row['status']].text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<Button v-else-if="column.dataIndex === 'operate'" aria-label="删除" theme="borderless" @click="removeRecord(record.key)">
<template #icon>
<IconDelete />
</template>
</Button>
</template>
<template #empty>
<Empty>
<template #image>
<IllustrationNoResult />
</template>
<template #darkModeImage>
<IllustrationNoResultDark />
</template>
<template #description>搜索无结果</template>
</Empty>
</template>
</Table>
</div>
</template>带分页组件的表格
表格分页目前支持两种模式:受控和非受控模式。
- 受控模式下,分页的状态完全由外部传入,依据为是否往 Table 传入了
pagination.currentPage这个字段。一般情况下,受控模式适用于远程拉取数据并渲染。 - 非受控模式下,Table 默认会将传入的
dataSource长度作为total传给 Pagination 组件,当然你也可以传入一个total字段来覆盖 Table 组件的取值,不过我们并不推荐用户在非受控分页模式下传入这个字段。
标题 | 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜 姜鹏志 | 2024-05-06 | ||
| 5 KB | 待评审 | 郝 郝宣 | 2024-05-11 | ||
| 10 KB | 已交付 | 姜 姜鹏志 | 2024-05-16 | ||
| 15 KB | 待评审 | 郝 郝宣 | 2024-05-21 | ||
| 20 KB | 已交付 | 姜 姜鹏志 | 2024-05-26 | ||
| 25 KB | 待评审 | 郝 郝宣 | 2024-05-31 | ||
| 30 KB | 已交付 | 姜 姜鹏志 | 2024-06-05 | ||
| 35 KB | 待评审 | 郝 郝宣 | 2024-06-10 | ||
| 40 KB | 已交付 | 姜 姜鹏志 | 2024-06-15 | ||
| 45 KB | 待评审 | 郝 郝宣 | 2024-06-20 |
代码
<script setup lang="ts">
import * as dateFns from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrous/semi-icons-vue';
import { Avatar, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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 getTag = (status: keyof typeof tagConfig) => tagConfig[status];
const DAY = 24 * 60 * 60 * 1000;
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: RowSelectionProps<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :rowSelection="rowSelection" :scroll="scroll">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<Tag v-else-if="column.dataIndex === 'status'" :color="getTag(text).color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="getTag(text).icon" />
</template>
{{ getTag(text).text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>拉取远程数据
正常情况下,数据往往不是一次性获取的,我们会在点击页码、过滤器或者排序按钮时从接口重新获取数据,这种情况下请使用受控模式来处理分页。用户需往 Table 传入 pagination.currentPage 这个字段,此时分页组件的渲染完全依赖于传入的 pagination 对象。
标题 | 交付状态 | 所有者 |
|---|
代码
<script setup lang="ts">
import { ref, watch } from 'vue';
import * as dateFns from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrous/semi-icons-vue';
import { Avatar, Table, Tag, type ColumnProps } from '@fibrous/semi-ui-vue';
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<Row>[] = [
{
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);
});
};
const dataSource = ref<Row[]>([]);
const loading = ref(true);
const currentPage = ref(1);
const handlePageChange = (page: number) => {
loading.value = true;
currentPage.value = page;
};
watch(
currentPage,
(page, _prev, onCleanup) => {
// Drop responses for pages the user has left
let ignore = false;
onCleanup(() => {
ignore = true;
});
void fetchData(page).then((rows) => {
if (!ignore) {
loading.value = false;
dataSource.value = rows;
}
});
},
{ immediate: true }
);
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :loading="loading" :pagination="{ currentPage, pageSize, total: data.length, onPageChange: handlePageChange }">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<Tag v-else-if="column.dataIndex === 'status'" :color="tagConfig[text as Row['status']].color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="tagConfig[text as Row['status']].icon" />
</template>
{{ tagConfig[text as Row['status']].text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>固定列或表头
可以通过设置 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 setup lang="ts">
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
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<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
fixed: true,
};
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :rowSelection="rowSelection" :scroll="scroll">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>通过 sticky 属性可以将表头固定在页面顶部。传入 top 时可以控制距离滚动容器的距离。
开启 sticky 后,Table 会自动打开 fixed 布局,列宽将由 column.width 决定。没有给定 width 的列宽由浏览器自动分配。
<script setup lang="ts">
import { onMounted, shallowRef } from 'vue';
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table } from '@fibrous/semi-ui-vue';
import type { ColumnProps, RowSelectionProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const columns: ColumnProps<Row>[] = [
{
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 dataSource = shallowRef<Row[]>([]);
const scroll = { y: 300, x: 1200 };
const sticky = { top: 60 };
const rowSelection: RowSelectionProps<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Michael James', // 这一行不可勾选
name: record.name,
}),
fixed: true,
};
onMounted(() => {
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().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
dataSource.value = data;
});
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :rowSelection="rowSelection" :scroll="scroll" :sticky="sticky">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>
带排序和过滤功能的表头
表格内部集成了过滤器和排序控件,用户可以通过在 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 setup lang="ts">
import * as dateFns from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrous/semi-icons-vue';
import { Avatar, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
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<Row>[] = [
{
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<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :rowSelection="rowSelection" :scroll="scroll">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<Tag v-else-if="column.dataIndex === 'status'" :color="tagConfig[text as Row['status']].color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="tagConfig[text as Row['status']].icon" />
</template>
{{ tagConfig[text as Row['status']].text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>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 setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: number | undefined;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const figmaIconUrl = '/assets/placeholder/app.svg';
const docIconUrl = '/assets/docs-icon.png';
const columns: ColumnProps<Row>[] = [
{
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>
<template>
<Table :columns="columns" :dataSource="dataSource">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text ? `${text} KB` : '未知' }}</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>自定义表头筛选
如果你需要将筛选器输入框展示在表头,可用 #headerCell 插槽(或 Table.Column 的 #title 插槽)渲染列标题,配合 filteredValue 使用。列的 title 仍是纯文本,插槽输出为空时显示它。
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| 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 setup lang="ts">
import { computed, ref } from 'vue';
import * as dateFns from 'date-fns';
import { IconClear, IconComment, IconTickCircle } from '@fibrous/semi-icons-vue';
import { Avatar, Input, Space, Table, Tag, type ColumnProps } from '@fibrous/semi-ui-vue';
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();
const filteredValue = ref<string[]>([]);
// composition makes the Input report the value once an IME composition ends
const handleChange = (value: string) => {
filteredValue.value = value ? [value] : [];
};
const columns = computed((): ColumnProps<Row>[] => [
{
title: '标题',
dataIndex: 'name',
width: 400,
onFilter: (value, record) => record.name.includes(value),
filteredValue: filteredValue.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),
},
]);
</script>
<template>
<Table :columns="columns" :dataSource="dataSource">
<template #headerCell="{ column, title }">
<Space v-if="column.dataIndex === 'name'">
<span>{{ title }}</span>
<Input composition placeholder="请输入筛选值" showClear :style="{ width: '200px' }" @change="handleChange" />
</Space>
</template>
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<Tag v-else-if="column.dataIndex === 'status'" :color="tagConfig[text as Row['status']].color" shape="circle" :style="{ userSelect: 'text' }">
<template #prefixIcon>
<component :is="tagConfig[text as Row['status']].icon" />
</template>
{{ tagConfig[text as Row['status']].text }}
</Tag>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>自定义筛选器
把列的 filterDropdown 设为 true,再用 #filterDropdown 插槽自定义渲染筛选器面板。插槽可以写在 Table 上(对所有 filterDropdown 为 true 的列调用,用 column 区分列),也可以写在 Table.Column 上;插槽输出为空时显示筛选菜单。
你可以在用户输入筛选值的时候调用 onTempFilteredValueChange 存储筛选值,在筛选值输入完毕后调用 onConfirm 触发真正的筛选。也可以通过 onConfirm({ filteredValue }) 直接筛选。
设置 tempFilteredValue 的原因是在需要存储临时筛选值的场景,不需要自己声明一个 ref 保存这个临时筛选值。
interface TableFilterDropdownSlotProps {
/** 当前列的配置 */
column: ColumnProps;
/** 筛选器配置项,列没有 `filters` 时为 `undefined` */
filters: TableFilter[] | undefined;
/** 临时筛选值,没有暂存值时为已生效的筛选值 */
tempFilteredValue: any[] | undefined;
/** 设置临时筛选值 */
onTempFilteredValueChange: (value: any[] | undefined) => void;
/** `onConfirm` 默认会将临时筛选值作为 `filteredValue` 并触发 `change` 事件。你也可以通过传入 `filteredValue` 直接设置筛选值 */
onConfirm: (options?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
/** 清除筛选值、临时筛选值 */
onClear: (options?: { closeDropdown?: boolean }) => void;
/** 关闭 dropdown */
onClose: () => void;
/** 把临时筛选值还原为已生效的筛选值 */
onReset: () => void;
}
filterDropdownProps 中的 onVisibleChange 不生效,筛选面板的开合通过列的 onFilterDropdownVisibleChange 获取。
标题 | 所有者 | ||
|---|---|---|---|
| 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 setup lang="ts">
import { nextTick, useTemplateRef } from 'vue';
import * as dateFns from 'date-fns';
import { Avatar, Button, Input, Space, Table, type ColumnProps, type InputHandle } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const nameInput = useTemplateRef<InputHandle>('nameInput');
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
onFilter: (value, record) => record.name.includes(value),
filterDropdown: true,
onFilterDropdownVisibleChange: () => {
// the panel is rendered after the visibility changes
void nextTick(() => nameInput.value?.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>
<template>
<Table :columns="columns" :dataSource="dataSource">
<template #filterDropdown="{ column, tempFilteredValue, onTempFilteredValueChange, onConfirm, onClear, onClose }">
<Space v-if="column.dataIndex === 'name'" align="start" :style="{ padding: '8px' }" vertical>
<Input ref="nameInput" :modelValue="tempFilteredValue?.[0]" @update:modelValue="onTempFilteredValueChange($event ? [$event] : [])" />
<Space>
<Button @click="onConfirm({ closeDropdown: true })">筛选 + 关闭</Button>
<Button @click="onClear({ closeDropdown: true })">清除 + 关闭</Button>
<Button @click="onClose()">直接关闭</Button>
</Space>
</Space>
<Space v-else align="start" :style="{ padding: '8px' }" vertical>
<Input :modelValue="tempFilteredValue?.[0]" @update:modelValue="onTempFilteredValueChange($event ? [$event] : [])" />
<Space>
<Button @click="onConfirm({ closeDropdown: false })">筛选后不关闭</Button>
<Button @click="onClear({ closeDropdown: false })">清除后不关闭</Button>
<Button @click="onClose()">直接关闭</Button>
</Space>
</Space>
</template>
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>筛选确认模式
通过设置 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 setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 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>
<template>
<Table :columns="columns" :dataSource="dataSource">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>自定义筛选项渲染
用 #filterDropdownItem 插槽自定义每个筛选项的渲染方式。插槽可以写在 Table 上,也可以写在 Table.Column 上(优先);插槽输出为空时显示默认筛选项。参数如下:
column: ColumnProps当前列的配置;text: string | undefined当前筛选项的文案;value: any当前筛选项的值;checked: boolean当前筛选项是否已经选中;filteredValue: any[]菜单当前显示的筛选值,确认模式下是尚未生效的临时值;level: number当前筛选项所处菜单的层级,顶层菜单为0;filterMultiple: boolean当前筛选项是否为多选;onChange: (event: MouseEvent) => void切换当前筛选项的选中状态,绑定到筛选项的click。
| 所有者 |
|---|
| 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 setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, DropdownItem, Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :rowSelection="rowSelection" :scroll="scroll">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
<template #filterDropdownItem="{ text, checked, onChange }">
<DropdownItem :active="checked" @click="onChange">{{ text }}</DropdownItem>
</template>
</Table>
</template>可以展开的表格
一般可展开行
如果需要渲染可以展开的表格,除了需要为 Table 提供 #expandedRow 插槽(参数 record、index、expanded,输出为空的行不渲染展开内容)外,还必须要指定 rowKey(默认为 key),Table 会根据 rowKey 取得行唯一标识符。展开的行可以用 v-model: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 setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Descriptions, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{ title: '标题', width: 500, dataIndex: 'name' },
{ 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<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
</script>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false" :rowSelection="rowSelection" rowKey="name">
<template #cell="{ column, text, record }">
<span v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
<template #expandedRow="{ index }">
<Descriptions align="justify" :data="expandData[index]">
<template #value="{ item }">
<Tag v-if="item.key === '垂类标签'" :style="{ margin: 0 }">{{ item.value }}</Tag>
<template v-else>{{ item.value }}</template>
</template>
</Descriptions>
</template>
</Table>
</template>展开按钮渲染为单独列
默认情况,展开按钮会与第一列文案渲染在同一个单元格内,你可以将 hideExpandedColumn 设为 false 来渲染为单独一列:
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| 2M | 姜 姜鹏志 | 2020-02-02 05:13 | ||||
| 2M | 郝 郝宣 | 2020-01-17 05:31 | ||||
| 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Descriptions, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{ title: '标题', width: 500, dataIndex: 'name' },
{ 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<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
</script>
<template>
<Table :columns="columns" :dataSource="data" :hideExpandedColumn="false" :pagination="false" :rowSelection="rowSelection" rowKey="name">
<template #cell="{ column, text, record }">
<span v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
<template #expandedRow="{ index }">
<Descriptions align="justify" :data="expandData[index]">
<template #value="{ item }">
<Tag v-if="item.key === '垂类标签'" :style="{ margin: 0 }">{{ item.value }}</Tag>
<template v-else>{{ item.value }}</template>
</template>
</Descriptions>
</template>
</Table>
</template>关闭某一行的可展开按钮渲染
可传入 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 setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Descriptions, Table, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{ title: '标题', width: 500, dataIndex: 'name' },
{ 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<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
const rowExpandable = (record: Row) => record.name !== '设计文档';
</script>
<template>
<Table :columns="columns" :dataSource="data" :hideExpandedColumn="false" :pagination="false" :rowExpandable="rowExpandable" :rowSelection="rowSelection" rowKey="name">
<template #cell="{ column, text, record }">
<span v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
<template #expandedRow="{ index }">
<Descriptions align="justify" :data="expandData[index]">
<template #value="{ item }">
<Tag v-if="item.key === '垂类标签'" :style="{ margin: 0 }">{{ item.value }}</Tag>
<template v-else>{{ item.value }}</template>
</template>
</Descriptions>
</template>
</Table>
</template>树形数据展示
表格支持树形数据的展示,当数据中有 children 字段时会自动展示为树形表格,如果不需要或使用其他字段可以用 childrenRecordName 进行配置。另外可以通过设置 indentSize 以控制每一层的缩进宽度。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
树形数据简单示例
| Key | 名称 | 数据类型 | 描述 | 默认值 |
|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
代码
<script setup lang="ts">
import { Table, type ColumnProps } from '@fibrous/semi-ui-vue';
interface TreeRow {
key?: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
}
const columns: ColumnProps<TreeRow>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
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>
<template>
<Table :columns="columns" :dataSource="data" defaultExpandAllRows />
</template>行可交换的树形数据
你可以通过改变 dataSource 元素的顺序来实现行交换操作。
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| status | 视频状态 | Enum <Integer> 枚举 | 视频的可见、推荐状态 | 1 | |
| vid | 视频 ID | String 字符串 | 标识视频的唯一 ID | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 | |
| title | 视频标题 | String 字符串 | 视频的标题 | 无 | |
| video_description | 视频描述 | String 字符串 | 视频的描述 | 无 |
代码
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { IconArrowDown, IconArrowUp } from '@fibrous/semi-icons-vue';
import { Button, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
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<TreeRow>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
{
key: 'operation',
},
];
const expandedRowKeys = ref<(string | number)[]>([1, 2]);
const data = shallowRef(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.value);
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.value = moveRecord(data.value, record[rowKey], offset);
};
</script>
<template>
<Table v-model:expandedRowKeys="expandedRowKeys" :childrenRecordName="childrenRecordName" :columns="columns" :dataSource="data" :rowKey="rowKey">
<template #cell="{ column, record }">
<template v-if="column.key === 'operation'">
<Button aria-label="上移" :disabled="!canMove(record, -1)" @click="move(record, -1)">
<template #icon>
<IconArrowUp />
</template>
</Button>
<Button aria-label="下移" :disabled="!canMove(record, 1)" @click="move(record, 1)">
<template #icon>
<IconArrowDown />
</template>
</Button>
</template>
</template>
</Table>
</template>树形选择
默认情况下,表格的行选中是各自独立的,你可以通过定义 selectedRowKeys 来模拟一个树形选中。
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
代码
<script setup lang="ts">
import { computed, ref } from 'vue';
import { pull, union } from 'es-toolkit';
import { Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
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<TreeRow>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
key: 121,
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
// Collects the keys of the rows and all their descendants.
const collectKeys = (rows: TreeRow[]): RowKey[] => rows.flatMap((row) => [row.key, ...collectKeys(row.children ?? [])]);
const selectedRowKeys = ref<RowKey[]>([]);
// Selecting a row selects its descendants too, and clearing it clears them.
const doSelect = (record: TreeRow, selected: boolean) => {
const keys = [record.key, ...collectKeys(record.children ?? [])];
selectedRowKeys.value = selected ? union(selectedRowKeys.value, keys) : pull([...selectedRowKeys.value], keys);
};
const doSelectAll = (selected: boolean) => {
selectedRowKeys.value = selected ? collectKeys(data) : [];
};
const rowSelection = computed<RowSelectionProps<TreeRow>>(() => ({
selectedRowKeys: selectedRowKeys.value,
onSelect: doSelect,
onSelectAll: doSelectAll,
}));
</script>
<template>
<Table :childrenRecordName="childrenRecordName" :columns="columns" :dataSource="data" :pagination="false" :rowKey="rowKey" :rowSelection="rowSelection" />
</template>树形选择关联(checkRelation)
通过设置 rowSelection.checkRelation 为 'related',可以实现父子节点选择关联。选中父节点会自动选中所有子节点,选中子节点会影响父节点的状态(全选/半选/未选)。
| Key | 名称 | 数据类型 | 描述 | |
|---|---|---|---|---|
| videos_info | 视频信息 | Object | 视频的元信息 | |
| text_info | 文本信息 | Object | 文本的元信息 |
代码
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
dataKey: string;
name: string;
type: string;
description: string;
children?: Row[];
}
const selectedRowKeys = ref<(string | number)[]>([]);
const columns: ColumnProps<Row>[] = [
{
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 = computed((): RowSelectionProps<Row> => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys) => {
selectedRowKeys.value = keys;
},
checkRelation: 'related',
}));
</script>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false" :rowSelection="rowSelection" />
</template>自定义行或单元格事件以及属性
- 传入
onRow/onHeaderRow可以定义表格或表头行的原生事件或属性。 - 传入
column.onCell/column.onHeaderCell可以定义表格或表头单元格原生事件或属性。
原则上 tr/td/th 上支持的属性或事件都能够被定义,返回对象使用 Vue 的命名:class、style、data-* 与监听器 onClick、onMouseenter、onDblclick、onContextmenu 等。拼错的键是类型错误。例如下面这个例子:
- 表头的 tr 定义了
onMouseenter/onMouseleave - 表格的 tr 定义了
class - 表格的第三行定义了
onClick
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜 姜鹏志 | 2024-05-06 | |
| 5 KB | 郝 郝宣 | 2024-05-11 | |
| 10 KB | 姜 姜鹏志 | 2024-05-16 | |
| 15 KB | 郝 郝宣 | 2024-05-21 | |
| 20 KB | 姜 姜鹏志 | 2024-05-26 | |
| 25 KB | 郝 郝宣 | 2024-05-31 | |
| 30 KB | 姜 姜鹏志 | 2024-06-05 | |
| 35 KB | 郝 郝宣 | 2024-06-10 | |
| 40 KB | 姜 姜鹏志 | 2024-06-15 | |
| 45 KB | 郝 郝宣 | 2024-06-20 |
代码
<script setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps, type TableProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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<Row>['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<Row>['onHeaderRow'] = (headerColumns, index) => ({
onMouseenter: () => console.log('mouse enter: ', headerColumns, index),
onMouseleave: () => console.log('mouse leave: ', headerColumns, index),
});
</script>
<template>
<Table :columns="columns" :dataSource="data" :onHeaderRow="onHeaderRow" :onRow="onRow">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>实现斑马纹样式
使用 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 setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, type ColumnProps, type TableProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
},
{
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<Row>['onRow'] = (_record, index) => (index % 2 === 0 ? { style: { background: 'var(--semi-color-fill-0)' } } : {});
</script>
<template>
<Table :columns="columns" :dataSource="data" :onRow="handleRow" :pagination="false">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' ? text.slice(0, 1) : '' }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>实现表头样式定制
可以通过 Column.onHeaderCell 返回特定 style 或 class,定制表头的样式
如下例子,通过传入 backgroundColor 改变了表头背景色
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜 姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝 郝宣 | 2020-01-17 05:31 | ||
| 34KB | Z Zoey Edwards | 2020-01-26 11:01 |
代码
<script setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const onHeaderCell = () => ({
style: {
backgroundColor: 'var(--semi-color-fill-0)',
},
});
const columns: ColumnProps<Row>[] = [
{
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>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>实现单元格 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 setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
},
{
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>
<template>
<Table class="component-table-demo-cell-hover-custom" :columns="columns" :dataSource="data" :pagination="false">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="record.nameIconSrc" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>单元格缩略
使用 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 setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Table, type ColumnProps } from '@fibrous/semi-ui-vue';
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>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false" :scroll="scroll">
<template #cell="{ column }">
<IconMore v-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>设置 ellipsis.showTitle 为 false 可以隐藏默认原生的 HTML title。配合 #cell 插槽可以自定义内容提示。
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| 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 setup lang="ts">
import { IconMore } from '@fibrous/semi-icons-vue';
import { Table, Typography, type ColumnProps } from '@fibrous/semi-ui-vue';
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>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false" :scroll="scroll">
<template #cell="{ column, text }">
<Typography.Text v-if="column.dataIndex === 'name' || column.dataIndex === 'owner'" :ellipsis="{ showTooltip: true }">
{{ text }}
</Typography.Text>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>可伸缩列
基本伸缩列
对于一些内容比较多的列,可以选择打开伸缩列功能,在表头进行拉拽实现列宽的实时变化。
不过你需要注意一些参数:
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 setup lang="ts">
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 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>
<template>
<Table bordered :columns="columns" :dataSource="data" resizable>
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
</Table>
</template>进阶的伸缩列
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 setup lang="ts">
import { normalizeClass } from 'vue';
import * as dateFns from 'date-fns';
import { addClass, removeClass } from '@fibrous/semi-foundation/utils/classnames';
import { Avatar, Table, type ColumnProps, type TableResizableProps } from '@fibrous/semi-ui-vue';
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<Row>[] = [
{
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<Row> = {
onResizeStart: (curColumn) => ({ class: addClass(normalizeClass(curColumn.class), 'my-resizing') }),
onResizeStop: (curColumn) => ({ class: removeClass(normalizeClass(curColumn.class), 'my-resizing') }),
};
</script>
<template>
<div id="components-table-demo-resizable-column">
<Table bordered :columns="columns" :dataSource="data" :pagination="pagination" :resizable="resizable">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</div>
</template>本例中使用的 CSS 样式定义:
#components-table-demo-resizable-column .my-resizing {
border-right: 2px solid red;
}
#components-table-demo-resizable-column .semi-table-resize-handle:hover {
background-color: red;
}
#components-table-demo-resizable-column .my-resizing:hover .semi-table-resize-handle {
background-color: inherit;
}
拖拽排序
使用 dnd-kit 的 @dnd-kit/vue 搭配 components 属性可轻松实现拖拽排序:components.body.row 传入一个 Vue 组件,它会收到每个数据行的属性与监听器(包括 data-row-key),在其中调用 useSortable 即可。
标题 | 所有者 | ||
|---|---|---|---|
| 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 setup lang="ts">
import { computed, defineComponent, h, shallowRef } from 'vue';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { arrayMove } from '@dnd-kit/helpers';
import { DragDropProvider, type DragDropProviderProps, type DragEndEvent } from '@dnd-kit/vue';
import { isSortable, useSortable } from '@dnd-kit/vue/sortable';
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps, type TableChangeInfo } from '@fibrous/semi-ui-vue';
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;
const dataSource = shallowRef(getData());
const pageNum = shallowRef(1);
const pageData = computed(() => dataSource.value.slice((pageNum.value - 1) * pageSize, pageNum.value * pageSize));
const columns: ColumnProps<Row>[] = [
{
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),
},
];
// Table creates a body row component per row and passes it the row's attributes, so the sortable must be set up
// inside it. A demo is a single SFC, hence the component is defined here.
const SortableRow = defineComponent((_props, { attrs, slots }) => {
const element = shallowRef<HTMLElement | null>(null);
// The sortable index is relative to the current page
const { isDragging } = useSortable({
id: () => String(attrs['data-row-key']),
index: () => pageData.value.findIndex((row) => row.key === attrs['data-row-key']),
element,
});
return () =>
h(
'tr',
{
ref: element,
class: { isDragging: isDragging.value },
style: {
cursor: 'grabbing',
...(isDragging.value ? { zIndex: 999, position: 'relative' as const } : {}),
},
},
slots.default?.()
);
});
const components = { body: { row: SortableRow } };
// Accessibility would add role="button" and a Tab stop to each row; keep table row semantics
const plugins: DragDropProviderProps['plugins'] = (defaults) => defaults.filter((plugin) => plugin !== Accessibility);
const modifiers = [RestrictToVerticalAxis];
const handleDragEnd = (event: DragEndEvent) => {
const { source } = event.operation;
if (event.canceled || !isSortable(source) || source.initialIndex === source.index) {
return;
}
const offset = (pageNum.value - 1) * pageSize;
dataSource.value = arrayMove(dataSource.value, offset + source.initialIndex, offset + source.index);
};
const handleChange = ({ pagination }: TableChangeInfo<Row>) => {
if (pagination?.currentPage) {
pageNum.value = pagination.currentPage;
}
};
</script>
<template>
<DragDropProvider :modifiers="modifiers" :plugins="plugins" @dragEnd="handleDragEnd">
<Table :columns="columns" :components="components" :dataSource="pageData" :pagination="{ currentPage: pageNum, pageSize, total: dataSource.length }" rowKey="key" @change="handleChange">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</DragDropProvider>
</template>表格分组
对于一些数据需要分组展示的表格,可以传入 groupBy 定义分组规则,使用 #groupSection 插槽(参数 groupKey、group)来定义分组表头的渲染;分组行唯一一列的列配置由 groupSectionColumn 返回。
注意:请务必为每行数据提供一个与其他行值不同的
key,或者使用rowKey参数指定一个作为主键的属性名。
标题 | 所有者 |
|---|
| 100 KB | 姜 姜鹏志 | 2024-08-14 | |
| 100 KB | 郝 郝宣 | 2024-08-14 | |
| 100 KB | 姜 姜鹏志 | 2024-08-14 | |
| 112 KB | 郝 郝宣 | 2024-08-26 | |
| 112 KB | 姜 姜鹏志 | 2024-08-26 | |
| 112 KB | 郝 郝宣 | 2024-08-26 | |
| 105 KB | 姜 姜鹏志 | 2024-08-19 | |
| 105 KB | 郝 郝宣 | 2024-08-19 | |
| 105 KB | 姜 姜鹏志 | 2024-08-19 | |
| 117 KB | 郝 郝宣 | 2024-08-31 |
代码
<script setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
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: Row) => `${record.owner.toLowerCase()}-${record.name.toLowerCase()}`;
const onGroupedRow = (group: { groupKey: string | number; records: Row[] }, index: number) => ({
onClick: () => {
console.log(`Grouped row clicked: `, group, index);
},
});
</script>
<template>
<div :style="{ padding: '20px 0px' }">
<!-- clickGroupedRowToExpand: a click anywhere on the group row expands it -->
<Table clickGroupedRowToExpand :columns="columns" :dataSource="data" groupBy="size" :onGroupedRow="onGroupedRow" :rowKey="rowKey" :scroll="scroll">
<template #groupSection="{ groupKey }">
<strong>根据文件大小分组 {{ groupKey }} KB</strong>
</template>
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</div>
</template>虚拟化表格
虚拟化可用于需要渲染大规模数据的场景,通过配置 virtualized 参数来开启这个功能。需要注意的是:
- 必须传递
scroll.y(number);表格宽度取style.width,未设置时取scroll.x。style是透传到根元素的样式,width写成带单位的字符串,如'750px';
- 需要传递每行的高度
virtualized.itemSize(不传时按size取默认行高:default为53,middle为45,small为37,组头行高与普通行相同),可以为如下类型:number(index, { sectionRow?: boolean, expandedRow?: boolean }) => number
- 表格分组虚拟化已支持
virtualized对象另外支持overscanCount:可视区上下额外渲染的行数,默认 2
通过 Table 的组件 ref 调用 scrollTo(offset) 与 scrollToItem(index, align),align 为 auto(默认)、smart、center、start、end;表格没有开启虚拟化时调用会在开发环境给出警告。
以下为渲染 1000 条数据的示例。
标题 | 所有者 |
|---|
代码
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import * as dateFns from 'date-fns';
import { Avatar, Button, Table, type ColumnProps, type TableHandle } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 200,
fixed: true,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 150,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
{
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' };
const table = useTemplateRef<TableHandle<Row>>('table');
</script>
<template>
<Button @click="table?.scrollToItem(100)">Scroll to 100</Button>
<Table ref="table" :columns="columns" :dataSource="data" :pagination="false" :scroll="scroll" :style="style" virtualized>
<template #cell="{ column, text, record }">
<template v-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' ? text.slice(0, 1) : '' }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}</template>
</template>
</Table>
</template>无限滚动
基于虚拟化特性,通过监听 @scroll 事件我们可以实现无限滚动加载数据。事件参数含 scrollDirection、scrollOffset 与 scrollUpdateWasRequested,只在纵向滚动偏移变化时触发。
标题 | 所有者 |
|---|
代码
<script setup lang="ts">
import { shallowRef } from 'vue';
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps, type TableVirtualizedProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 200,
fixed: true,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 150,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
},
{
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;
};
const data = shallowRef(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.value = [...data.value, ...getPage(data.value.length)];
};
const onScroll = ({ scrollDirection, scrollOffset, scrollUpdateWasRequested }: { scrollDirection: 'forward' | 'backward'; scrollOffset: number; scrollUpdateWasRequested: boolean }) => {
if (scrollDirection === 'forward' && scrollOffset >= (data.value.length - Math.ceil(scroll.y / itemSize) * 1.5) * itemSize && !scrollUpdateWasRequested) {
loadMore();
}
};
</script>
<template>
<Table :columns="columns" :dataSource="data" :pagination="false" :scroll="scroll" :style="style" :virtualized="virtualized" @scroll="onScroll">
<template #cell="{ column, text, record }">
<div v-if="column.dataIndex === 'name'">{{ text }}</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>受控的动态表格
标题 | 所有者 |
|---|
代码
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef } from 'vue';
import * as dateFns from 'date-fns';
import { Avatar, Button, ButtonGroup, Switch, Table, type ColumnProps, type RowSelectionProps, type TableChangeInfo, type TablePaginationPosition, type TablePaginationProps, type TableScroll } from '@fibrous/semi-ui-vue';
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 = { marginBottom: '15px', display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap' } as const;
const switchStyle = { display: 'inline-flex', alignItems: '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].sort(sorter);
if (sortOrder === 'descend') {
result = result.reverse();
}
}
return result;
};
const fetchData = (pagination: PageState, currentPage = 1, sorter: ColumnProps = {}, filters: ColumnProps[] = []) => {
const nextPagination = { ...pagination, currentPage };
return new Promise<FetchResult>((resolve) => {
setTimeout(() => {
let data = sortData(sorter, allData);
data = filterData(filters, data);
const dataSource = data.slice((currentPage - 1) * nextPagination.pageSize, currentPage * nextPagination.pageSize);
nextPagination.total = data.length;
resolve({
dataSource,
pagination: nextPagination,
sorter,
filters,
});
}, 1500);
});
};
const getSorterColumn = (columns: ColumnProps[]) => columns.filter((column) => !!column.sorter)[0];
const getFilterColumns = (columns: ColumnProps[]) => columns.filter((column) => Array.isArray(column.filteredValue) && column.filteredValue.length);
const loading = ref(true);
const columns = shallowRef(initialColumns);
const scroll = shallowRef<TableScroll>({});
const rowSelection = shallowRef<RowSelectionProps>();
const expandable = ref(false);
const expandCellFixed = ref(false);
const showTitle = ref(false);
const showFooter = ref(false);
const expandedRowKeys = ref<(string | number)[]>([]);
const showHeader = ref(true);
const bordered = ref(false);
const resizable = ref(false);
const pagination = shallowRef<PageState | false>(defaultPagination);
const dataSource = shallowRef<Row[]>([]);
const load = (currentPagination: PageState, currentPage?: number, sorter?: ColumnProps, filters?: ColumnProps[]) => {
fetchData(currentPagination, currentPage, sorter, filters)
.then((result) => {
let nextColumns = mergeColumns(result.sorter, columns.value);
for (const filterObj of result.filters) {
nextColumns = mergeColumns(filterObj, nextColumns);
}
columns.value = nextColumns;
loading.value = false;
pagination.value = result.pagination;
dataSource.value = result.dataSource;
})
.catch((err) => {
console.error(err);
loading.value = false;
});
};
const setPage = (page?: number | null, sorter?: ColumnProps | null, filters?: ColumnProps[]) => {
if (loading.value) {
return;
}
const currentPage = typeof page === 'number' ? page : (pagination.value && pagination.value.currentPage) || 1;
loading.value = true;
load(pagination.value || defaultPagination, currentPage, sorter || getSorterColumn(columns.value), filters || getFilterColumns(columns.value));
};
onMounted(() => {
load(defaultPagination, 1, getSorterColumn(initialColumns), []);
});
const tablePagination = computed(() => pagination.value && { ...pagination.value, onPageChange: setPage });
const toggleFixHeader = (checked: boolean) => {
scroll.value = { ...scroll.value, y: checked ? 300 : undefined };
};
const toggleFixColumns = (checked: boolean) => {
const lastIndex = columns.value.length - 1;
if (checked) {
columns.value = columns.value.map((column, index) => {
if (index === 0) {
return { ...column, fixed: true };
}
return index === lastIndex ? { ...column, fixed: 'right' } : column;
});
} else {
columns.value = columns.value.map((column) => ({ ...column, fixed: false }));
}
if (rowSelection.value) {
rowSelection.value = { ...rowSelection.value, fixed: checked };
}
scroll.value = { ...scroll.value, x: checked ? '150%' : undefined };
expandCellFixed.value = checked;
};
const toggleRowSelection = (checked: boolean) => {
rowSelection.value = checked
? {
width: 48,
fixed: true,
onChange: (selectedRowKeys, selectedRows) => console.log('Selection changed, selectedRowKeys: ', selectedRowKeys, 'selectedRows: ', selectedRows),
}
: undefined;
};
const toggleShowSorter = (checked: boolean) => {
columns.value = columns.value.map((column) => ({
...column,
sorter: checked ? initialColumns.find((item) => item.dataIndex === column.dataIndex)?.sorter : undefined,
}));
};
const toggleShowFilter = (checked: boolean) => {
if (checked) {
columns.value = columns.value.map((column) =>
column.dataIndex === 'name'
? {
...column,
filters: [
{
text: '姓名中包含 1',
value: '1',
},
{
text: '姓名中包含 2',
value: '2',
},
{
text: '姓名中包含 3',
value: '3',
},
],
filteredValue: [],
}
: column
);
} else {
columns.value = columns.value.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.value = dataSource.value.filter((data) => data.key).map((data) => data.key);
expandable.value = true;
} else {
expandedRowKeys.value = [];
}
};
const toggleResizable = (checked: boolean) => {
resizable.value = checked;
bordered.value = checked;
};
const toggleDataSource = (checked: boolean) => {
if (checked) {
dataSource.value = [];
} else {
setPage();
}
};
const switchPagination = (position: TablePaginationPosition | false) => {
pagination.value = position ? { ...defaultPagination, ...pagination.value, position } : false;
};
const switches: SwitchItem[] = [
{ text: '固定表头:', checked: () => !!scroll.value.y, onChange: toggleFixHeader },
{
text: '隐藏表头:',
onChange: (checked) => {
showHeader.value = !checked;
},
},
{
text: '显示标题:',
onChange: (checked) => {
showTitle.value = checked;
},
},
{
text: '显示底部:',
onChange: (checked) => {
showFooter.value = checked;
},
},
{ text: '固定列:', onChange: toggleFixColumns },
{ text: '显示选择列:', onChange: toggleRowSelection },
{
text: '显示加载状态:',
checked: () => loading.value,
onChange: (checked) => {
loading.value = checked;
},
},
{ text: '无数据:', checked: () => !dataSource.value.length, onChange: toggleDataSource },
{
text: '开启排序功能:',
checked: () => columns.value.some((column) => column.sorter),
onChange: toggleShowSorter,
},
{
text: '开启过滤功能:',
checked: () => columns.value.some((column) => column.filters),
onChange: toggleShowFilter,
},
{
text: '开启行展开功能:',
checked: () => expandable.value,
onChange: (checked) => {
expandable.value = checked;
},
},
{ text: '展开当前所有行:', onChange: toggleExpandedRowKeys },
{
text: '显示边框:',
checked: () => bordered.value,
onChange: (checked) => {
bordered.value = checked;
},
},
{ text: '开启列伸缩功能:', onChange: toggleResizable },
];
</script>
<template>
<div>
<div :style="wrapStyle">
<span v-for="item in switches" :key="item.text" :style="switchStyle">
<span>{{ item.text }}</span>
<Switch :aria-label="item.text" :modelValue="item.checked?.()" size="small" @change="item.onChange" />
</span>
<span :style="switchStyle">
<span>分页控件:</span>
<ButtonGroup>
<Button @click="switchPagination('bottom')">Bottom</Button>
<Button @click="switchPagination('top')">Top</Button>
<Button @click="switchPagination('both')">Both</Button>
<Button @click="switchPagination(false)">None</Button>
</ButtonGroup>
</span>
</div>
<Table
v-model:expandedRowKeys="expandedRowKeys"
:bordered="bordered"
:columns="columns"
:dataSource="dataSource"
:expandCellFixed="expandCellFixed"
:loading="loading"
:pagination="tablePagination"
:resizable="resizable"
:rowSelection="rowSelection"
:scroll="scroll"
:showHeader="showHeader"
:title="showTitle ? 'This is title.' : undefined"
@change="onChange"
>
<template #cell="{ column, record, text }">
<span v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
<template v-if="expandable" #expandedRow="{ record }">
<p>{{ record.description }}</p>
</template>
<template v-if="showFooter" #footer>
<p :style="{ margin: 0 }">This is footer.</p>
</template>
</Table>
</div>
</template>完全自定义渲染
一般情况下,使用 #cell 插槽即可,但是你也可以通过传递 Column.useFullRender=true 来开启完全自定义渲染模式,此时插槽的输出会替换整个单元格,复选框按钮、展开按钮、缩进等节点将会传给 #headerCell(或 Table.Column 的 #title)与 #cell 插槽,你可以进一步来定义表头和单元格的内容渲染方式。
其中 #headerCell 的参数为:
{
column: ColumnProps,
title: string | undefined, // 列标题
filter: VNode | null, // 筛选按钮
sorter: VNode | null, // 排序按钮
selection: VNode | null, // 选择按钮
}
#cell 的参数中与完全自定义渲染有关的部分为:
{
expandIcon: VNode | null, // 展开按钮
selection: VNode | null, // 选择按钮
indentText: VNode | null, // 缩进
}
使用 <component :is="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 setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '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<Row> = {
hidden: true,
fixed: 'left',
};
const onChange = (changeInfo: unknown) => console.log(changeInfo);
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :pagination="pagination" :rowSelection="rowSelection" @change="onChange">
<template #headerCell="{ column, title, sorter, filter, selection }">
<span v-if="column.dataIndex === 'name'" :style="{ display: 'inline-flex', alignItems: 'center', paddingLeft: '20px' }">
<component :is="selection" />
<span :style="{ marginLeft: '8px' }">{{ title }}</span>
<component :is="sorter" />
<component :is="filter" />
</span>
</template>
<template #cell="{ column, text, record, expandIcon, selection, indentText }">
<span v-if="column.dataIndex === 'name'" :style="{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }">
<component :is="indentText" />
<component :is="expandIcon" />
<component :is="selection" />
<span :style="{ marginLeft: '8px' }">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
</span>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
<template #expandedRow="{ record }">
<article>{{ record.name }}</article>
</template>
</Table>
</template>表头合并
用户可以通过表头合并功能进行表头的分组,表头合并支持与固定列、虚拟化、数据分组、列伸缩等功能复合使用,也同时支持 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 setup lang="ts">
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '基本信息',
fixed: 'left',
children: [
{
title: '标题',
dataIndex: 'name',
width: 300,
fixed: true,
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>
<template>
<Table :columns="columns" :dataSource="data" :rowSelection="rowSelection" :scroll="scroll" @change="onChange">
<template #cell="{ column, text, record }">
<span v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
<IconMore v-else-if="column.dataIndex === 'operate'" />
</template>
<template #expandedRow="{ record }">
<article>{{ record.name }}</article>
</template>
</Table>
</template>合并表头 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 setup lang="ts">
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-vue';
import { Avatar, Table, TableColumn as Column } from '@fibrous/semi-ui-vue';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = (total: number) => {
const data: Row[] = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData(46);
const rowSelection = { fixed: true };
const scroll = { x: 1000, y: 400 };
const nameFilters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const filterName = (value: string, record: Row) => record.name.includes(value);
const sortSize = (a: Row, b: Row) => (a.size - b.size > 0 ? 1 : -1);
const sortUpdateTime = (a: Row, b: Row) => (a.updateTime - b.updateTime > 0 ? 1 : -1);
const onChange = (changeInfo: unknown) => console.log(changeInfo);
</script>
<template>
<Table :dataSource="data" :rowSelection="rowSelection" :scroll="scroll" @change="onChange">
<Column fixed="left" title="基本信息">
<Column dataIndex="name" :filters="nameFilters" fixed :onFilter="filterName" title="标题" :width="300">
<template #cell="{ text }">
<span>
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</span>
</template>
</Column>
<Column dataIndex="size" fixed :sorter="sortSize" title="大小" :width="100">
<template #cell="{ text }">{{ text }} KB</template>
</Column>
</Column>
<Column title="其他信息">
<Column dataIndex="owner" title="所有者">
<template #cell="{ text, record }">
<div>
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
</template>
</Column>
<Column dataIndex="updateTime" :sorter="sortUpdateTime" title="更新日期">
<template #cell="{ text }">{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}</template>
</Column>
</Column>
<Column align="center" dataIndex="operate" fixed="right" title="更多" :width="100">
<template #cell>
<IconMore />
</template>
</Column>
<template #expandedRow="{ record }">
<article>{{ record.name }}</article>
</template>
</Table>
</template>行列合并
- 表头除了通过
children写法进行合并外,可通过设置column.colSpan进行表头的列合并。 - 表格支持行/列合并,由
column.onCell返回单元格属性colSpan或者rowSpan,设值为 0 时,该单元格不会渲染。
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 setup lang="ts">
import * as dateFns from 'date-fns';
import { Avatar, Table, type ColumnProps } from '@fibrous/semi-ui-vue';
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';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 4 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
},
{
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>
<template>
<Table :columns="columns" :dataSource="dataSource" :pagination="false">
<template #cell="{ column, record, text }">
<div v-if="column.dataIndex === 'name'">
<Avatar alt="" shape="square" size="small" :src="figmaIconUrl" :style="{ marginRight: '12px' }" />
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'size'">{{ text }} KB</template>
<div v-else-if="column.dataIndex === 'owner'">
<Avatar :color="record.avatarBg" size="small" :style="{ marginRight: '4px' }">
{{ typeof text === 'string' && text.slice(0, 1) }}
</Avatar>
{{ text }}
</div>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ dateFns.format(new Date(text), 'yyyy-MM-dd') }}
</template>
</template>
</Table>
</template>API 参考
Table
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| bordered | 是否展示外边框和列边框。 | boolean | undefined |
| childrenRecordName | 树形表格中,数据项里表示子级数据的字段名。 | string | undefined |
| clickGroupedRowToExpand | 点击分组行时展开或收起该组。 | boolean | undefined |
| columns | 列配置,详见 Column。传入时(包括空数组)优先于 Table.Column 子项。 | TypeColumnProps<R>[] | undefined |
| components | 覆盖 Table 的组成元素,如 table、body、row、td、th,值为标签名或 Vue 组件,组件会收到被替换元素的属性与监听器。 | TableComponents | undefined |
| dataSource | 数据。请为每一条数据分配一个独立的 key,或使用 rowKey 指定一个作为主键的属性名。原地增删元素(push、splice)会重新排序和筛选;只修改记录字段时,单元格显示新值,但不重新排序和筛选。 | R[] | undefined |
| defaultExpandAllGroupRows | 默认是否展开所有分组行,动态加载数据时不生效。 | boolean | undefined |
| defaultExpandAllRows | 默认是否展开所有行,动态加载数据时不生效。 | boolean | undefined |
| defaultExpandedRowKeys | 默认展开的行 key 数组,动态加载数据时不生效。 | (string | number)[] | undefined |
| direction | 文字方向,默认值取 ConfigProvider 的 direction,可在此单独配置。 | "ltr" | "rtl" | semiGlobalDefault("Table", "direction") |
| expandAllGroupRows | 是否展开所有分组行。 | boolean | undefined |
| expandAllRows | 是否展开所有行。 | boolean | undefined |
| expandCellFixed | 展开按钮所在列是否固定,取值与 Column 的 fixed 相同。 | boolean | "left" | "right" | undefined |
| expandIcon | 只接受 false,隐藏展开按钮。自定义展开图标使用 expandIcon 插槽。 | boolean | undefined |
| expandedRowKeys | 展开的行 key 数组,传入后行展开受控,配合 v-model:expandedRowKeys 使用。 | (string | number)[] | undefined |
| expandRowByClick | 点击行时是否展开可展开行。 | boolean | undefined |
| footer | 纯文本表格尾部,footer 插槽优先。 | string | undefined |
| groupBy | 分组依据,可以是数据项中的字段名,或返回字符串、数字的函数。 | TypeTableGroupBy<R> | undefined |
| groupSectionColumn | 分组行唯一一列的列配置,返回值不含 title、children、dataIndex。 | Type((groupKey: string | number, group: (string | number)[]) => Omit<ColumnProps<R>, "title" | "children" | "dataIndex">) | undefined |
| headerStyle | 所有表头单元格(包括固定表头)的内联样式。 | CSSProperties | undefined |
| hideExpandedColumn | 表格可展开时,展开按钮默认与第一列内容渲染在同一个单元格;设为 false 时展开按钮单独占一列。 | boolean | undefined |
| indentSize | 树形数据每一层的缩进宽度,单位为像素。 | number | undefined |
| keepDOM | 折叠行时是否保留被折叠的 DOM。 | boolean | undefined |
| loading | 是否显示加载状态。 | boolean | undefined |
| pagination | 分页配置,传 false 关闭分页。 | boolean | TablePaginationProps | undefined |
| prefixCls | 样式名前缀。 | string | undefined |
| resizable | 是否开启列宽伸缩,需要伸缩的列必须提供 width。传入对象时可配置 onResize、onResizeStart、onResizeStop 与 handlerClassName。 | Typeboolean | ResizableProps<R> | undefined |
| rowExpandable | 判断某一行是否可展开,返回 false 时不渲染该行的展开按钮。 | TypeTableRowExpandable<R> | undefined |
| rowKey | 行 key 的取值:数据项的字段名,或返回字符串、数字的函数。 | TypeTableRowKey<R> | undefined |
| rowSelection | 行选择配置,传 true 使用默认配置,详见 rowSelection。 | Typeboolean | RowSelectionProps<R> | undefined |
| rowSpanHover | 悬停在带 rowSpan 的单元格上时,高亮它跨越的所有行。 | boolean | undefined |
| scroll | 滚动区域配置,x 为横向宽度,y 为纵向高度,详见 scroll。 | TableScroll | undefined |
| showHeader | 是否显示表头。 | boolean | undefined |
| size | 表格尺寸,影响单元格的内边距。 | "small" | "default" | "middle" | undefined |
| sticky | 将表头固定在页面顶部,传对象时 top 为距离滚动容器顶部的距离。 | TableSticky | undefined |
| title | 纯文本表格标题,title 插槽优先。 | string | undefined |
| virtualized | 开启虚拟化,需同时传 scroll.y(数字)。对象形式可配置行高 itemSize 与额外渲染的行数 overscanCount。 | TableVirtualized | undefined |
| onGroupedRow | 类似 onRow,单独用于分组行,返回值是 tr 的属性与监听器。index 在未开启 virtualized 时是分组序号,开启后是该行在虚拟列表中的位置。 | Type((record: { groupKey: string | number; records: R[]; }, index: number) => TableRowAttrs) | undefined |
| onHeaderRow | 设置表头行的属性与监听器,返回值会合并到表头行(不含 style)。 | Type((columns: ColumnProps<R>[], index: number) => Omit<TableRowAttrs, "style">) | undefined |
| onRow | 设置数据行的属性与监听器,第三个参数为行状态。返回的监听器在行自身的处理和 rowXxx 事件之后调用。 | Type((record: R, index: number, rowState: { disabled: boolean; selected: boolean; }) => TableRowAttrs) | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| scroll | 虚拟化表体的纵向偏移变化时触发,横向滚动不触发。 | [args: VirtualListScrollArgs] |
| change | 分页、排序、筛选变化时触发,参数含 pagination、filters、sorter 与 extra.changeType。 | Type[changeInfo: TableChangeInfo<R>] |
| expand | 点击展开按钮时触发,参数为展开状态、行数据(分组行为 groupKey 对象)和鼠标事件。 | Type[expanded: boolean, record: TableIncludeGroupRecord<R>, event: MouseEvent] |
| expandedRowsChange | 展开的行变化时触发,参数为展开的行数据数组。 | Type[expandedRows: TableIncludeGroupRecord<R>[]] |
| update:expandedRowKeys | 展开的行变化时触发,参数为展开的行 key 数组,包含已展开的分组 key,用于 v-model:expandedRowKeys。 | [expandedRowKeys: (string | number)[]] |
| rowMouseEnter | 鼠标进入数据行时触发,先于 onRow 返回的 onMouseenter 调用。 | [record: R, event: MouseEvent] |
| rowMouseLeave | 鼠标离开数据行时触发,先于 onRow 返回的 onMouseleave 调用。 | [record: R, event: MouseEvent] |
| rowDoubleClick | 双击数据行时触发,先于 onRow 返回的 onDblclick 调用。 | [record: R, event: MouseEvent] |
| rowContextMenu | 数据行触发右键菜单时触发,先于 onRow 返回的 onContextmenu 调用。 | [record: R, event: MouseEvent] |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | Table.Column 子项,传入 columns 时被忽略。 | any |
| cell | 数据单元格的内容,对所有数据列调用,用 column.dataIndex 或 column.key 区分列;输出为空时显示原始值。Table.Column 自己的 cell 插槽优先。 | TypeTableCellSlotProps<R> |
| headerCell | 列标题的内容;输出为空时显示 column.title。Table.Column 的 title 插槽优先。 | TypeTableHeaderCellSlotProps<R> |
| title | 表格标题,参数 pageData 为当前页数据。 | TypeTableTitleSlotProps<R> |
| footer | 表格尾部,参数 pageData 为当前页数据。 | TypeTableTitleSlotProps<R> |
| empty | 无数据时展示的内容,不提供时显示语言包的空文案,提供空插槽时不显示任何内容。 | any |
| expandedRow | 展开行的内容,参数含 record、index、expanded;输出为空时该行不渲染。 | TypeTableExpandedRowSlotProps<R> |
| groupSection | 分组行的内容,参数含 groupKey 与 group(该组的行 key)。 | TableGroupSectionSlotProps |
| sortIcon | 排序图标,参数含 sortOrder 与 column;输出为空时显示默认箭头。Table.Column 自己的 sortIcon 插槽优先。 | TypeTableSortIconSlotProps<R> |
| filterIcon | 筛选按钮的内容,参数含 filtered 与 column;输出为空时显示默认图标。Table.Column 自己的 filterIcon 插槽优先。 | TypeTableFilterIconSlotProps<R> |
| filterDropdown | 筛选面板,用于 filterDropdown 为 true 的列,参数含临时筛选值与 onConfirm、onClear、onClose、onReset;输出为空时显示筛选菜单。 | TypeTableFilterDropdownSlotProps<R> |
| filterDropdownItem | 筛选菜单中的一项,参数含 text、value、checked、level、onChange 等;输出为空时显示默认项。 | TypeTableFilterDropdownItemSlotProps<R> |
| selectionCell | 选择列的单元格,表头与数据行都调用,参数含 selected、inHeader、originNode 等;输出为空时显示默认复选框。 | TypeTableSelectionCellSlotProps<R> |
| expandIcon | 展开按钮的图标,参数 expanded;输出为空时显示默认图标。自定义图标不带旋转动画。 | TableExpandIconSlotProps |
| pagination | 替换分页信息与分页器,参数 pagination 可直接 v-bind 给 Pagination;输出为空时显示默认分页。 | TablePaginationSlotProps |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style、id、data-* 等原生属性经 attrs 落在最外层元素;aria-label、aria-labelledby、aria-describedby 落在 role="table" 的元素上(表头与表体分离或虚拟化时是它们的共同容器,否则是表体的 table)。有标题而使用方没有传名称时,表格通过 aria-labelledby 指向标题。
TableProps、TableEmits、TableSlots、TableHandle 与各数据类型都可以从 @fibrous/semi-ui-vue 导入,RecordType 是它们的泛型参数 R,默认为 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 的值是标签名或 Vue 组件,组件会收到被替换元素的属性与监听器,默认插槽是元素的内容。可以替换的位置(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 setup lang="ts">
import { Table } from '@fibrous/semi-ui-vue';
import type { ColumnProps } from '@fibrous/semi-ui-vue';
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>
<template>
<Table :columns="columns" :dataSource="dataSource" :scroll="{ x: 900 }" :headerStyle="{ backgroundColor: '#F5F6F7', fontWeight: 600 }" />
</template>
一些上面用到的类型定义,均可从 @fibrous/semi-ui-vue 导入:
// PaginationProps 为 Pagination 组件支持的 props
interface TablePaginationProps extends PaginationProps {
position?: 'top' | 'bottom' | 'both';
formatPageText?: boolean | ((pageInfo: { currentStart: number; currentEnd: number; total: number }) => string);
onChange?: (currentPage: number, pageSize: number) => void;
onPageChange?: (currentPage: number) => void;
onPageSizeChange?: (pageSize: number) => void;
}
RecordType 为 Table 和 Column 的泛型参数 R,默认为 Record<string, any>。你可以这样使用:
<script setup lang="ts">
import { Table } from '@fibrous/semi-ui-vue';
import type { ColumnProps } from '@fibrous/semi-ui-vue';
interface Row {
key: string;
name: string;
age: number;
address: string;
}
const columns: ColumnProps<Row>[] = [
{ title: 'Name', dataIndex: 'name', width: 200 },
// ...
];
const data: Row[] = [
{ key: '1', name: 'John Brown', age: 32, address: 'New York No. 1 Lake Park' },
// ...
];
</script>
<template>
<Table :columns="columns" :dataSource="data" />
</template>
onHeaderRow / onRow 用法
onHeaderRow 中可以返回 th 支持的属性或者事件 onRow 中可以返回 tr 支持的属性或者事件
onRow 的第三个参数 rowStatus 可以获取当前行的状态信息,包括 disabled 和 selected 属性。这在需要根据行的选中或禁用状态执行不同逻辑时非常有用,例如点击行时判断是否允许选中。
返回对象里的事件监听按 Vue 的 onXxx 命名(onClick、onMouseenter、onMouseleave),样式类用 class。
<script setup lang="ts">
import { Table } from '@fibrous/semi-ui-vue';
const onRow = (record, index, rowStatus) => ({
onClick: () => {
// rowStatus.disabled 表示当前行是否被禁用
// rowStatus.selected 表示当前行是否被选中
if (rowStatus.disabled) {
return;
}
console.log('点击行', index, rowStatus);
},
onMouseenter: () => {}, // 鼠标移入行
onMouseleave: () => {}, // 鼠标移出行
class: '',
// 其他可以作用于 tr 的属性或事件
});
const onHeaderRow = (columns, index) => ({
onClick: () => {}, // 点击表头行
class: '',
// 其他可以作用于 tr 的属性或事件
});
</script>
<template>
<Table :columns="columns" :dataSource="dataSource" :onRow="onRow" :onHeaderRow="onHeaderRow" />
</template>
Column
列配置只接受下表的属性,拼错的属性名会报类型错误。需要在列上携带自定义字段时,用交叉类型扩展,如 ColumnProps<RecordType> & { tooltip: string }。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | 设置列的对齐方式,在 RTL 时会自动切换 | 'left' | 'right' | 'center' | 'left' |
| class | 列样式名 | string | object | array | |
| 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 插槽提供,需要自行编写各种交互 | boolean | |
| filterDropdownProps | 透传给 Dropdown 的属性与事件监听(on 开头的键),触发方式固定为点击,详情点击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 内容和排序按钮的名称。需要自定义内容时用 #headerCell 插槽 | string | |
| useFullRender | 是否完全自定义渲染,用法详见完全自定义渲染,开启此功能会造成一定的性能损耗 | boolean | false |
| width | 列宽度 | string | number | |
| onCell | 设置单元格属性 | (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 |
单元格、表头、排序与筛选图标和筛选项的自定义渲染用 Table 的插槽:#cell、#headerCell、#sortIcon、#filterIcon、#filterDropdown、#filterDropdownItem,参数见上文 Table 的 Slots。Table.Column 写法时,这些插槽也可以写在该列上(表头用 #title 代替 #headerCell),优先于 Table 上的同名插槽;写了 #filterDropdown 的列等同于设置了 filterDropdown: true,嵌套的 Table.Column 写在默认插槽里作为分组子列。没有 render、renderFilterDropdown、renderFilterDropdownItem、shouldCellUpdate。
一些上面用到的类型定义:
type Filter = {
value: any;
text: string;
children?: Filter[];
};
Column.onCell / onHeaderCell 用法
与 onRow、onHeaderRow类似,在 column.onCell column.onHeaderCell 中也能返回 td/th 支持的属性或事件
rowSelection
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| class | 所处列样式名 | string | object | array | |
| 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 插槽,不再有 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 | - |
| defaultCurrentPage | 默认的当前页码 | number | 1 |
| 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 的 change 事件不再响应分页器变化 | (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 |
方法
通过组件 ref 可以访问到 Table 暴露的方法,类型为 TableHandle:
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue';
import { Table } from '@fibrous/semi-ui-vue';
import type { TableHandle } from '@fibrous/semi-ui-vue';
const table = useTemplateRef<TableHandle>('table');
onMounted(() => {
table.value?.getCurrentPageData(); // => { dataSource: [/*...*/], groups: /*...*/ }
});
</script>
<template>
<Table ref="table" :columns="columns" :dataSource="dataSource" />
</template>
| 名称 | 描述 |
|---|---|
| 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;名称为「筛选此列数据」,有筛选值时为「筛选此列数据,筛选已生效」。#filterIcon插槽只替换按钮内的图标,按钮的名称和键盘操作不变
- 固定表头(
scroll.y、sticky)或虚拟化时,表头和表体渲染为两个 table 元素,二者的共同容器是 role 为 table 的元素,内部 table 为 role none,单元格仍与列头关联 - 虚拟化表格的
aria-rowcount为表头行数加数据总行数,数据行的aria-rowindex接在表头行之后,屏幕阅读器据此报出不在 DOM 中的行
已知限制:
- 带排序的列,
title渲染在排序按钮里,title中不要放按钮、链接等可交互内容
RTL/LTR
- Table 的 RTL 默认值为 ConfigProvider direction,可以通过 Table direction 覆盖
- Table 列的 align 与 fixed 属性会在 RTL 时会自动切换,left <-> right
- Table 的树形数据暂不支持 RTL(Chrome、Safari 浏览器表现与 Firefox 表现不同)
文案规范
- 表格标题
- 表格标题应清晰的让用户感知到表格的目的;
- 为复杂表格添加描述,为用户提供更多关于表格的上下文信息;
- 使用句子大小写;
- 列标题
- 保持列标题简洁,建议使用 1-2 个词作为列标题;
- 当列标题较长时,建议 2 行显示,剩余文字缩略并在 Tooltip 中显示完全;
- 采用 Sentence case 的大小写规则;
- 列标题使用句子大小写;
- 表格操作
- 可以遵循 Button 的文案规范
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration-table_body-bg | var(--semi-transition_duration-none) | 表格-背景颜色-动画持续时间 |
| $transition_function-table_body-bg | var(--semi-transition_function-easeOut) | 表格-背景颜色-过渡曲线 |
| $transition_delay-table_body-bg | 0ms | 表格-背景颜色-延迟时间 |
| $transition_duration-table_row-head-bg | var(--semi-transition_duration-none) | 表格-行头-背景颜色-动画持续时间 |
| $transition_function-table_row-head-bg | linear | 表格-行头-背景颜色-过渡曲线 |
| $spacing-table-paddingY | $spacing-base | 表格空数据占位及虚拟列表分组垂直内边距 |
| $spacing-table-paddingX | $spacing-base-tight | 表格空数据占位及虚拟列表分组水平内边距 |
| $spacing-table_middle-paddingY | $spacing-base-tight | 中尺寸表格单元格垂直内边距 |
| $spacing-table_small-paddingY | $spacing-tight | 小尺寸表格单元格垂直内边距 |
| $spacing-table_title-paddingY | $spacing-base | 表格标题垂直内边距 |
| $spacing-table_title-paddingX | 0 | 表格标题水平内边距 |
| $spacing-table_footer-padding | $spacing-base | 表格 footer 内边距 |
| $spacing-table_expand_row-paddingLeft | 16px | 表格展开行左侧内边距 |
| $spacing-table_expand_row-paddingRight | 16px | 表格展开行右侧内边距 |
| $spacing-table_resizable-offset-y | 4px | 可拖动拉伸操作垂直偏移量 |
| $spacing-table_resizable-bottom | 4px | 可拖动拉伸操作底部距离 |
| $spacing-table_row_head-paddingY | $spacing-base-tight | 表头垂直内边距 |
| $spacing-table_row_head-paddingX | $spacing-base | 表头水平内边距 |
| $spacing-table_react_resizable_handle-right | -1px | 可拖动拉伸操作向右偏移量 |
| $spacing-table_tbody_rowCell-padding | $spacing-base | 表格体单元格内边距(默认尺寸) |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingX | 16px | 表格选择状态行(非复选框列)单元格水平内边距 |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingY | 10px | 表格选择状态行(非复选框列)垂直内边距 |
| $spacing-table_column_sorter-marginLeft | $spacing-extra-tight | 表头标题与排序按钮间距 |
| $spacing-table_column_filter-marginLeft | $spacing-extra-tight | 列过滤器按钮左侧外边距 |
| $spacing-table_bordered_titler-paddingLeft | $spacing-base | 带边框表格标题左侧内边距 |
| $spacing-table_bordered_titler-paddingRight | $spacing-base | 带边框表格标题右侧内边距 |
| $spacing-table_expand_icon-marginRight | 8px | 行展开按钮右侧外边距 |
| $spacing-table_panel_operation-paddingX | $spacing-base | 表格操作栏水平内边距 |
| $spacing-table_panel_operation-paddingY | $spacing-tight | 表格操作栏垂直内边距 |
| $width-table_base_border | 1px | 表格单元格分割线宽度 |
| $width-table_header_border | 1px | 表格表头分割线宽度 |
| $width-table_resizer_border | 2px | 表格拉伸列标示线宽度 |
| $width-table_column_selection | 48px | 表格选择列及展开列宽度 |
| $width-table_column_sorter-icon | 16px | 表格排序按钮宽度 |
| $height-table_column_sorter-icon | 16px | 表格排序按钮高度 |
| $width-table_column_filter-icon | 16px | 表格过滤按钮宽度 |
| $height-table_column_filter-icon | 16px | 表格过滤按钮高度 |
| $width-table_cell_fixed_left_last | 1px | 表格左侧固定列右侧分割线宽度 |
| $width-table_cell_fixed_right_first | 1px | 表格右侧固定列左侧分割线宽度 |
| $width-table_react_resizable_handle | 9px | 表格伸缩列调节热区宽度 |
| $height-table_pagination_outer_min | 60px | 表格分页器高度 |
| $height-table_column_filter_dropdown | 290px | 表格过滤筛选列表最大高度 |
| $color-table_panel-bg-default | var(--semi-color-primary) | 操作区域样式默认背景颜色 |
| $color-table_panel-text-default | var(--semi-color-primary-light-active) | 表格操作栏选中项文本颜色 |
| $color-table-bg-default | var(--semi-color-bg-1) | 表格固定列及吸顶表头背景颜色 |
| $color-table-text-default | var(--semi-color-text-0) | 单元格默认文字颜色 |
| $color-table-border-default | var(--semi-color-border) | 表格描边颜色 |
| $color-table_shadow-bg-default | rgb(from var(--semi-black) r g b / 0.08) | 表格滚动后阴影颜色 |
| $color-table_shadow-bg-dark | rgb(from var(--semi-black) r g b / 0.4) | 表格滚动后阴影颜色 - 暗色模式 |
| $color-table_shadow-border-default | var(--semi-color-border) | 表格拟阴影 描边颜色 |
| $color-table_th-bg-default | var(--semi-color-bg-1) | 表头背景底色,须不透明以遮住滚动到固定列和吸顶表头下的内容 |
| $color-table_th-fill-default | var(--semi-color-fill-0) | 表头背景叠加的填充色 |
| $color-table_th-border-default | var(--semi-color-border) | 表头底部分割线颜色 |
| $color-table_th-text-default | var(--semi-color-text-2) | 表头文字颜色 |
| $color-table_th-clickSort-bg-hover | var(--semi-color-fill-1) | 点击表头触发排序背景色 - 悬浮 |
| $color-table_pl-bg-default | transparent | |
| $color-table_body-bg-default | var(--semi-color-bg-1) | 表格行背景颜色 - 默认态 |
| $color-table_body-bg-hover | var(--semi-color-fill-0) | 表格行背景颜色 - 悬浮态 |
| $color-table_footer-bg-default | var(--semi-color-fill-0) | 表格 footer 背景颜色 - 默认 |
| $color-table_row_expanded-bg-default | var(--semi-color-fill-0) | 表格展开行背景颜色 - 默认 |
| $color-table_expanded_icon-default | var(--semi-color-text-2) | 表格展开行图标颜色 - 默认 |
| $color-table_expanded-bg-default | transparent | 表格展开行图标容器背景颜色 - 默认 |
| $color-table_filter_on-text-default | var(--semi-color-primary) | 表格过滤按钮颜色 - 激活态 |
| $color-table_filter-text-default | var(--semi-color-text-2) | 表格过滤按钮颜色 - 默认态 |
| $color-table_sorter_on-text-default | var(--semi-color-primary) | 表格排序按钮颜色 - 激活态 |
| $color-table_sorter-text-default | var(--semi-color-text-2) | 表格排序按钮颜色 - 默认态 |
| $color-table_sorter-text-hover | var(--semi-color-text-2) | 表格排序按钮颜色 - 悬浮态 |
| $color-table_page-text-default | var(--semi-color-text-2) | 表格翻页器文本颜色 |
| $color-table_resizer-bg-default | var(--semi-color-primary) | 表格拉伸标示线颜色 |
| $color-table_selection-bg-default | var(--semi-grey-0) | 表格分组背景色 |
| $color-table_placeholder-text-default | var(--semi-color-text-2) | 表格空数据文本颜色 |
| $color-table_cell-bg-hover | var(--semi-color-bg-0) | 让表格在 hover 时正确显示 $color-table_body-bg-hover 颜色,如无必要不要修改 |
| $font-table_base-fontSize | 14px | 表格默认文本字号 |
| $border-table_base-borderStyle | solid | 表格描边样式 |
| $width-table_fixed_shadow | 6px | 表格固定列滚动阴影宽度 |
| $z-table_fixed_column | $z-table-fixed | fixed列的zIndex值 |
FAQ
-
点击第二页的行选择按钮,会跳转到第一页?
Table 的 dataSource 更新后,会将页码重置到初始态。请检查数据源是否在组件渲染时发生了变化。
Vue 的 Table 逐项比较新旧
dataSource,每一项都是原来的记录时不会重置,新增或删除记录会重置。不要在模板里写字面量,也不要在每次渲染时重新生成记录对象。<script setup lang="ts"> import { shallowRef } from 'vue'; const dataSource = shallowRef<Record<string, any>[]>([]); // 只在需要时整体替换 const load = async () => { dataSource.value = await fetchData(); }; </script> <template> <!-- 错误:每次渲染都会生成新的记录对象 --> <Table :dataSource="list.map((item) => ({ ...item }))" :columns="columns" /> </template> -
筛选后的数据条数不对?
请检查你的筛选列和数据源是否配置正确。
筛选列需设置独立的 dataIndex,同时 dataSource 需要设置独立的 key,请参考 dataSource API。否则筛选功能无法正常工作。
-
表格数据为何没有更新?
Vue 通过响应式追踪变化:
dataSource用ref或shallowRef保存,替换整个数组或调用push、splice等方法都会触发更新;shallowRef下直接修改某条记录的字段不会触发,需要整体替换数组。columns同理,依赖响应式数据时用computed,不依赖时定义在script setup顶层,避免写在模板里的字面量。 -
为何我的表格行不能选中以及展开?
请指定 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 暂不支持单行选择功能,用户可以通过自定义方式实现单选。