代码演示
如何引入
<script setup lang="ts">
import { List, ListItem } from '@fibrous/semi-ui-vue';
</script>
模板中 ListItem 也可以写成 List.Item。
基本用法
列表的基本用法。可以通过 size 设置尺寸,支持large, default, small。通过 #header 和 #footer 插槽自定义列表头部和尾部,通过 #item="{ item, index }" 作用域插槽渲染 dataSource 的每一项。
Default Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Small Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
Large Size
- 从明天起,做一个幸福的人
- 喂马,劈柴,周游世界
- 从明天起,关心粮食和蔬菜
- 我有一所房子,面朝大海,春暖花开
代码
<script setup lang="ts">
import { List, ListItem } from '@fibrous/semi-ui-vue';
const data = ['从明天起,做一个幸福的人', '喂马,劈柴,周游世界', '从明天起,关心粮食和蔬菜', '我有一所房子,面朝大海,春暖花开'];
</script>
<template>
<div>
<div :style="{ marginRight: '16px' }">
<h3 :style="{ marginBottom: '16px' }">Default Size</h3>
<List bordered :dataSource="data">
<template #header>
<div>Header</div>
</template>
<template #footer>
<div>Footer</div>
</template>
<template #item="{ item }">
<ListItem>{{ item }}</ListItem>
</template>
</List>
</div>
<div :style="{ marginRight: '16px' }">
<h3 :style="{ margin: '16px 0' }">Small Size</h3>
<List bordered :dataSource="data" size="small">
<template #header>
<div>Header</div>
</template>
<template #footer>
<div>Footer</div>
</template>
<template #item="{ item }">
<ListItem>{{ item }}</ListItem>
</template>
</List>
</div>
<div :style="{ marginRight: '16px' }">
<h3 :style="{ margin: '16px 0' }">Large Size</h3>
<List bordered :dataSource="data" size="large">
<template #header>
<div>Header</div>
</template>
<template #footer>
<div>Footer</div>
</template>
<template #item="{ item }">
<ListItem>{{ item }}</ListItem>
</template>
</List>
</div>
</div>
</template>模板用法
列表的 List.Item 内置了简单的结构,通过 #header、#main 和 #extra 插槽提供。其中 header 和 main 的对齐方式可以通过 align 属性设置,支持 flex-start(默认), flex-end, center, baseline, 和 stretch 。
- SE示例标题
- SE示例标题
Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SE示例标题
Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。
代码
<script setup lang="ts">
import { Avatar, Button, ButtonGroup, List } from '@fibrous/semi-ui-vue';
const text = 'Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。';
const data = [
{
text,
style: {
color: 'var(--semi-color-text-2)',
margin: '4px 0',
width: '420px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
},
{ text, style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: '500px' } },
{
text: 'Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。',
style: { color: 'var(--semi-color-text-2)', margin: '4px 0', width: '500px' },
},
];
</script>
<template>
<div :style="{ padding: '12px', border: '1px solid var(--semi-color-border)', margin: '12px' }">
<List :dataSource="data">
<template #item="{ item }">
<List.Item>
<template #header>
<Avatar color="blue">SE</Avatar>
</template>
<template #main>
<div>
<span :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">示例标题</span>
<p :style="item.style">{{ item.text }}</p>
</div>
</template>
<template #extra>
<ButtonGroup theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</template>
</List.Item>
</template>
</List>
</div>
</template>布局
通过 layout 属性可以设置列表的布局,支持vertical(默认)和horizontal。
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 3
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
代码
<script setup lang="ts">
import { Avatar, List, ListItem, type AvatarColor } from '@fibrous/semi-ui-vue';
interface Item {
title: string;
color: AvatarColor;
}
const data: Item[] = [
{ title: 'Semi Design Title 1', color: 'light-blue' },
{ title: 'Semi Design Title 2', color: 'grey' },
{ title: 'Semi Design Title 3', color: 'light-green' },
];
</script>
<template>
<div :style="{ padding: '12px', border: '1px solid var(--semi-color-border)', margin: '12px' }">
<List :dataSource="data" layout="horizontal">
<template #item="{ item }">
<ListItem>
<template #header>
<Avatar :color="item.color">SE</Avatar>
</template>
<template #main>
<div>
<span :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">{{ item.title }}</span>
<p :style="{ color: 'var(--semi-color-text-2)', margin: '4px 0' }">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
</template>
</ListItem>
</template>
</List>
</div>
</template>栅格列表
通过 grid 属性可以实现栅格列表,span 可设置每项的占格数,gutter可设置栅格间隔。
Semi UI
| 满意度 | |
|---|---|
| 反馈数 | 124 |
Semi DSM
| 满意度 | |
|---|---|
| 反馈数 | 108 |
Semi D2C
| 满意度 | |
|---|---|
| 反馈数 | 244 |
代码
<script setup lang="ts">
import { Button, ButtonGroup, Descriptions, DescriptionsItem, List, ListItem, Rating } from '@fibrous/semi-ui-vue';
const data = [
{ title: 'Semi UI', rating: 4.5, feedbacks: 124 },
{ title: 'Semi DSM', rating: 4, feedbacks: 108 },
{ title: 'Semi D2C', rating: 4.5, feedbacks: 244 },
];
const grid = { gutter: 12, span: 6 };
const itemStyle = {
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-2)',
borderRadius: '3px',
paddingLeft: '20px',
};
</script>
<template>
<div>
<List :dataSource="data" :grid="grid">
<template #item="{ item }">
<ListItem :style="itemStyle">
<div>
<h3 :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">{{ item.title }}</h3>
<Descriptions align="center" row size="small">
<DescriptionsItem>
<template #itemKey>满意度</template>
<Rating allowHalf :modelValue="item.rating" size="small" />
</DescriptionsItem>
<DescriptionsItem>
<template #itemKey>反馈数</template>
{{ item.feedbacks }}
</DescriptionsItem>
</Descriptions>
<div :style="{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }">
<ButtonGroup :style="{ marginTop: '8px' }" theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</div>
</div>
</ListItem>
</template>
</List>
</div>
</template>响应式的栅格列表
响应式的栅格列表。响应尺寸与 Grid 保持一致。
审核管理平台
| 满意度 | |
|---|---|
| 反馈数 | 124 |
扁鹊
| 满意度 | |
|---|---|
| 反馈数 | 108 |
直播审核平台
| 满意度 | |
|---|---|
| 反馈数 | 244 |
抖音安全测试
| 满意度 | |
|---|---|
| 反馈数 | 189 |
内容平台
| 满意度 | |
|---|---|
| 反馈数 | 128 |
策略平台
| 满意度 | |
|---|---|
| 反馈数 | 156 |
代码
<script setup lang="ts">
import { Button, ButtonGroup, Descriptions, DescriptionsItem, List, Rating } from '@fibrous/semi-ui-vue';
const data = [
{ title: '审核管理平台', rating: 4.5, feedbacks: 124 },
{ title: '扁鹊', rating: 4, feedbacks: 108 },
{ title: '直播审核平台', rating: 3.5, feedbacks: 244 },
{ title: '抖音安全测试', feedbacks: 189 },
{ title: '内容平台', rating: 3, feedbacks: 128 },
{ title: '策略平台', rating: 4, feedbacks: 156 },
];
const grid = { gutter: 12, xs: 0, sm: 0, md: 12, lg: 8, xl: 8, xxl: 6 };
const style = {
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-2)',
borderRadius: '3px',
paddingLeft: '20px',
margin: '8px 2px',
};
</script>
<template>
<div>
<List :dataSource="data" :grid="grid">
<template #item="{ item }">
<List.Item :style="style">
<div>
<h3 :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">{{ item.title }}</h3>
<Descriptions align="center" row size="small">
<DescriptionsItem>
<template #itemKey>满意度</template>
<Rating allowHalf :modelValue="item.rating" size="small" />
</DescriptionsItem>
<DescriptionsItem>
<template #itemKey>反馈数</template>
{{ item.feedbacks }}
</DescriptionsItem>
</Descriptions>
<div :style="{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }">
<ButtonGroup :style="{ marginTop: '8px' }" theme="borderless">
<Button>编辑</Button>
<Button>更多</Button>
</ButtonGroup>
</div>
</div>
</List.Item>
</template>
</List>
</div>
</template>加载更多
可通过 #loadMore 插槽实现加载更多的功能。
代码
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { Avatar, Button, List, Skeleton } from '@fibrous/semi-ui-vue';
interface Book {
title?: string;
loading: boolean;
}
const count = 3;
const data: Book[] = [];
for (let i = 0; i < 40; i++) {
data.push({ title: `Semi Design Title ${i}`, loading: false });
}
const placeholders: Book[] = [0, 1, 2].map(() => ({ loading: true }));
const fetchData = (page: number) =>
new Promise<Book[]>((resolve) => {
setTimeout(() => resolve(data.slice(page * count, page * count + count)), 1000);
});
const dataSource = ref<Book[]>([]);
const loading = ref(true);
const noMore = ref(false);
let page = 0;
let disposed = false;
onBeforeUnmount(() => {
disposed = true;
});
const load = async () => {
loading.value = true;
const newDataSource = await fetchData(page);
if (disposed) return;
dataSource.value = [...dataSource.value, ...newDataSource];
loading.value = false;
noMore.value = !newDataSource.length;
};
const onLoadMore = () => {
page += 1;
void load();
};
void load();
</script>
<template>
<List :dataSource="loading ? [...dataSource, ...placeholders] : dataSource" :loading="loading">
<template #item="{ item }">
<Skeleton :loading="item.loading">
<template #placeholder>
<div
:style="{
display: 'flex',
alignItems: 'flex-start',
padding: '12px',
borderBottom: '1px solid var(--semi-color-border)',
}"
>
<Skeleton.Avatar :style="{ marginRight: '12px' }" />
<div>
<Skeleton.Title :style="{ width: '120px', marginBottom: '12px', marginTop: '12px' }" />
<Skeleton.Paragraph :rows="2" :style="{ width: '600px' }" />
</div>
</div>
</template>
<List.Item>
<template #header>
<Avatar color="grey">SE</Avatar>
</template>
<template #main>
<div>
<span :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">{{ item.title }}</span>
<p :style="{ color: 'var(--semi-color-text-2)', margin: '4px 0' }">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
</template>
</List.Item>
</Skeleton>
</template>
<template v-if="!loading && !noMore" #loadMore>
<div :style="{ textAlign: 'center', marginTop: '12px', height: '32px', lineHeight: '32px' }">
<Button @click="onLoadMore">显示更多</Button>
</div>
</template>
</List>
</template>滚动加载
可以监听滚动容器的 scroll 事件,在接近底部时加载下一页。交互建议符合 semi 交互设计规范,这里采用三次滚加载后出现 load more 按钮的形式。
代码
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { Avatar, Button, List, Spin } from '@fibrous/semi-ui-vue';
interface Book {
title: string;
}
const count = 5;
const dataList: Book[] = [];
for (let i = 0; i < 100; i++) {
dataList.push({ title: `Semi Design Title ${i}` });
}
const fetchData = (page: number) =>
new Promise<Book[]>((resolve) => {
setTimeout(() => resolve(dataList.slice(page * count, page * count + count)), 1000);
});
const dataSource = ref<Book[]>([]);
const loading = ref(true);
const hasMore = ref(true);
const page = ref(0);
let disposed = false;
onBeforeUnmount(() => {
disposed = true;
});
const load = async () => {
loading.value = true;
const newDataSource = await fetchData(page.value);
if (disposed) return;
dataSource.value = [...dataSource.value, ...newDataSource];
loading.value = false;
hasMore.value = !!newDataSource.length;
};
const onLoadMore = () => {
page.value += 1;
void load();
};
const showLoadMore = () => (page.value + 1) % 4 === 0;
const onScroll = (event: Event) => {
const el = event.currentTarget as HTMLElement;
if (loading.value || !hasMore.value || showLoadMore()) return;
if (el.scrollHeight - el.scrollTop - el.clientHeight <= 20) onLoadMore();
};
void load();
</script>
<template>
<section aria-label="可滚动列表" class="light-scrollbar" :style="{ height: '420px', overflow: 'auto', border: '1px solid var(--semi-color-border)', padding: '10px' }" tabindex="0" @scroll="onScroll">
<List :dataSource="dataSource">
<template #item="{ item }">
<List.Item>
<template #header>
<Avatar color="grey">SE</Avatar>
</template>
<template #main>
<div>
<span :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">{{ item.title }}</span>
<p :style="{ color: 'var(--semi-color-text-2)', margin: '4px 0' }">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
</template>
</List.Item>
</template>
<template v-if="!loading && hasMore && showLoadMore()" #loadMore>
<div :style="{ textAlign: 'center', marginTop: '12px', height: '32px', lineHeight: '32px' }">
<Button @click="onLoadMore">显示更多</Button>
</div>
</template>
</List>
<div v-if="loading && hasMore" :style="{ textAlign: 'center' }">
<Spin />
</div>
</section>
</template>滚动加载无限长列表
可以通过集成 TanStack Virtual 实现滚动加载无限长列表,带有虚拟化(virtualization)功能,只渲染可见的行,能够提高数据量大时候长列表的性能。
代码
<script setup lang="ts">
import { computed, shallowReactive, useTemplateRef, watch } from 'vue';
import { useVirtualizer } from '@tanstack/vue-virtual';
import { Avatar, List, Spin } from '@fibrous/semi-ui-vue';
interface Book {
title: string;
}
const rowCount = 50;
const rowHeight = 118;
// Rows fetched ahead of the window so they arrive before they scroll into view
const prefetch = 15;
const dataList: Book[] = Array.from({ length: rowCount }, (_, i) => ({ title: `Semi Design Title ${i}` }));
const scroller = useTemplateRef('scroller');
const loaded = shallowReactive(new Set<number>());
// A requested index is never fetched again, even while pending
const requested = new Set<number>();
const virtualizer = useVirtualizer({ count: rowCount, getScrollElement: () => scroller.value, estimateSize: () => rowHeight, overscan: 2 });
const items = computed(() => virtualizer.value.getVirtualItems());
const rows = computed(() => items.value.filter((item) => loaded.has(item.index)));
watch(items, (current) => {
if (!current.length) return;
const wanted: number[] = [];
for (let i = current[0].index; i <= Math.min(current[current.length - 1].index + prefetch, rowCount - 1); i++) {
if (!requested.has(i)) {
requested.add(i);
wanted.push(i);
}
}
if (!wanted.length) return;
setTimeout(() => {
for (const i of wanted) loaded.add(i);
}, 1000);
});
</script>
<template>
<div :style="{ border: '1px solid var(--semi-color-border)', padding: '10px' }">
<section ref="scroller" aria-label="虚拟滚动列表" :style="{ height: '500px', overflow: 'auto' }" tabindex="0">
<List :style="{ position: 'relative', height: `${virtualizer.getTotalSize()}px` }">
<!-- Replaces the locale's empty text until a row of the window arrives -->
<template #emptyContent>
<Spin />
</template>
<List.Item v-for="item in rows" :key="item.index" :style="{ position: 'absolute', insetInline: '0', top: `${item.start}px`, height: `${rowHeight}px` }">
<template #header>
<Avatar color="grey">SE</Avatar>
</template>
<template #main>
<div>
<span :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">{{ dataList[item.index].title }}</span>
<p :style="{ color: 'var(--semi-color-text-2)', margin: '4px 0' }">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
</template>
</List.Item>
</List>
</section>
</div>
</template>拖拽排序
使用 dnd-kit 的 Vue 版(@dnd-kit/vue)可轻松实现拖拽排序。被拖动的就是 ListItem 渲染的 li,#item 返回的每一项要写 :key,拖动重排 DOM 后 Vue 才能把节点对应到正确的数据。
- SESemi Design Title 1
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 2
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 3
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 4
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
- SESemi Design Title 5
Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。
代码
<script setup lang="ts">
import { defineComponent, h, shallowRef, type ComponentPublicInstance, type SlotsType, type VNode } from 'vue';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { move } from '@dnd-kit/helpers';
import { DragDropProvider, type DragDropProviderProps, type DragEndEvent } from '@dnd-kit/vue';
import { useSortable } from '@dnd-kit/vue/sortable';
import { Avatar, List, ListItem, type AvatarColor } from '@fibrous/semi-ui-vue';
interface Item {
id: number;
title: string;
color: AvatarColor;
}
const listItems = shallowRef<Item[]>([
{ id: 1, title: 'Semi Design Title 1', color: 'red' },
{ id: 2, title: 'Semi Design Title 2', color: 'grey' },
{ id: 3, title: 'Semi Design Title 3', color: 'light-green' },
{ id: 4, title: 'Semi Design Title 4', color: 'light-blue' },
{ id: 5, title: 'Semi Design Title 5', color: 'pink' },
]);
// useSortable must run inside the item's own component; the li itself is dragged, so items stay direct children.
// A demo is a single SFC, hence the component is defined here.
const SortableListItem = defineComponent({
props: {
id: { type: Number, required: true },
index: { type: Number, required: true },
},
slots: Object as SlotsType<{ header: () => VNode[]; main: () => VNode[] }>,
setup(props, { slots }) {
const element = shallowRef<ComponentPublicInstance | null>(null);
const { isDragging } = useSortable({ id: () => props.id, index: () => props.index, element });
return () =>
h(
ListItem,
{
ref: (instance) => {
element.value = instance as ComponentPublicInstance | null;
},
class: { isDragging: isDragging.value },
style: {
border: '1px solid var(--semi-color-border)',
marginBottom: '12px',
cursor: 'grabbing',
...(isDragging.value ? { zIndex: 999, position: 'relative', backgroundColor: 'var(--semi-color-bg-0)' } : {}),
},
},
{ header: slots.header, main: slots.main }
);
},
});
// Accessibility would add role="button" and a Tab stop to each li; pointer dragging only
const plugins: DragDropProviderProps['plugins'] = (defaults) => defaults.filter((plugin) => plugin !== Accessibility);
const modifiers = [RestrictToVerticalAxis];
const handleDragEnd = (event: DragEndEvent) => {
if (!event.canceled) {
listItems.value = move(listItems.value, event);
}
};
</script>
<template>
<div :style="{ padding: '12px', border: '1px solid var(--semi-color-border)', margin: '12px' }">
<DragDropProvider :modifiers="modifiers" :plugins="plugins" @dragEnd="handleDragEnd">
<List :dataSource="listItems">
<template #item="{ item, index }">
<!-- dnd-kit reorders li nodes mid-drag; an index key would hand a node another item's data -->
<SortableListItem :id="item.id" :key="item.id" :index="index">
<template #header>
<Avatar :color="item.color">SE</Avatar>
</template>
<template #main>
<div>
<span :style="{ color: 'var(--semi-color-text-0)', fontWeight: 500 }">
{{ item.title }}
</span>
<p :style="{ color: 'var(--semi-color-text-2)', margin: '4px 0' }">Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。</p>
</div>
</template>
</SortableListItem>
</template>
</List>
</DragDropProvider>
</div>
</template>带分页器
你可以组合使用 Pagination, 实现一个分页的 List
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 1/3
代码
<script setup lang="ts">
import { computed, ref } from 'vue';
import { List, Pagination } from '@fibrous/semi-ui-vue';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数'];
const pageSize = 4;
const page = ref(1);
const pageData = computed(() => data.slice((page.value - 1) * pageSize, page.value * pageSize));
</script>
<template>
<div>
<div :style="{ marginRight: '16px', width: '280px', display: 'flex', flexWrap: 'wrap' }">
<List class="component-list-demo-booklist" :dataSource="pageData" size="small" :split="false" :style="{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }">
<template #item="{ item }">
<List.Item class="list-item">{{ item }}</List.Item>
</template>
</List>
<Pagination v-model:currentPage="page" :pageSize="pageSize" size="small" :style="{ width: '100%', flexBasis: '100%', justifyContent: 'center' }" :total="data.length" />
</div>
</div>
</template>带筛选器
你可以通过组装 Input 使用,实现对 List 列表的筛选
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
- 被讨厌的勇气
- 罪与罚
代码
<script setup lang="ts">
import { shallowRef } from 'vue';
import { IconSearch } from '@fibrous/semi-icons-vue';
import { Input, List, ListItem } from '@fibrous/semi-ui-vue';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
const list = shallowRef(data);
const onSearch = (keyword?: string) => {
list.value = keyword ? data.filter((item) => item.includes(keyword)) : data;
};
</script>
<template>
<div>
<div
:style="{
marginRight: '16px',
width: '280px',
display: 'flex',
flexWrap: 'wrap',
border: '1px solid var(--semi-color-border)',
}"
>
<List class="component-list-demo-booklist" :dataSource="list" size="small" :split="false" :style="{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }">
<template #header>
<Input placeholder="搜索" @change="(value) => !value && onSearch()" @compositionend="(event) => onSearch((event.target as HTMLInputElement).value)">
<template #prefix>
<IconSearch />
</template>
</Input>
</template>
<template #item="{ item }">
<ListItem class="list-item">{{ item }}</ListItem>
</template>
</List>
</div>
</div>
</template>添加删除项
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
代码
<script setup lang="ts">
import { shallowRef } from 'vue';
import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-vue';
import { Button, List } from '@fibrous/semi-ui-vue';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
const list = shallowRef(data.slice(0, 8));
const removeItem = (item: string) => {
list.value = list.value.filter((book) => book !== item);
};
const addItem = () => {
list.value = list.value.concat(data.slice(list.value.length, list.value.length + 1));
};
</script>
<template>
<div>
<div
:style="{
marginRight: '16px',
width: '280px',
display: 'flex',
flexWrap: 'wrap',
border: '1px solid var(--semi-color-border)',
}"
>
<List class="component-list-demo-booklist" :dataSource="list" size="small" :split="false" :style="{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }">
<template #item="{ item }">
<li :key="item" class="list-item" :style="{ margin: '4px' }">
<Button :aria-label="`删除${item}`" :style="{ marginRight: '4px' }" theme="borderless" type="danger" @click="removeItem(item)">
<template #icon>
<IconMinusCircle />
</template>
</Button>
{{ item }}
</li>
</template>
</List>
<div :style="{ margin: '4px', fontSize: '14px' }">
<Button aria-label="新增书籍" :style="{ marginRight: '4px', color: 'var(--semi-color-info)' }" theme="borderless" @click="addItem">
<template #icon>
<IconPlusCircle />
</template>
</Button>
新增书籍
</div>
</div>
</div>
</template>单选或多选
你可以通过组合使用 Radio 或 Checkbox 将 List 增强为一个列表选择器
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
代码
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Checkbox, CheckboxGroup, List, Radio, RadioGroup, type CheckboxValue, type RadioValue } from '@fibrous/semi-ui-vue';
const data = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数'];
const pageData = computed(() => data.slice(0, 8));
const checkboxVal = ref<CheckboxValue[]>([data[0]]);
const radioVal = ref<RadioValue | undefined>(data[0]);
const listStyle = { border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 };
</script>
<template>
<div :style="{ display: 'flex' }">
<div :style="{ marginRight: '16px', width: '280px', display: 'flex', flexWrap: 'wrap' }">
<CheckboxGroup v-model="checkboxVal">
<List class="component-list-demo-booklist" :dataSource="pageData" size="small" :split="false" :style="listStyle">
<template #item="{ item }">
<List.Item class="list-item">
<Checkbox :value="item">{{ item }}</Checkbox>
</List.Item>
</template>
</List>
</CheckboxGroup>
</div>
<div :style="{ marginRight: '16px', width: '280px', display: 'flex', flexWrap: 'wrap' }">
<RadioGroup v-model="radioVal">
<List class="component-list-demo-booklist" :dataSource="pageData" size="small" :split="false" :style="listStyle">
<template #item="{ item }">
<List.Item class="list-item">
<Radio :value="item">{{ item }}</Radio>
</List.Item>
</template>
</List>
</RadioGroup>
</div>
</div>
</template>响应键盘事件
你可以自行监听对应按键的键盘事件,实现不同 Item 的选择。如下面这个例子,可以使用上下方向键选择不同Item
- 围城
- 平凡的世界(全三册)
- 三体(全集)
- 雪中悍刀行(全集)
- 撒哈拉的故事
- 明朝那些事
- 一禅小和尚
- 沙丘
- 被讨厌的勇气
- 罪与罚
代码
<script setup lang="ts">
import { onBeforeUnmount, onMounted, shallowRef } from 'vue';
import { List, ListItem } from '@fibrous/semi-ui-vue';
const list = ['围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚'];
const hoverIndex = shallowRef(-1);
const changeIndex = (offset: number) => {
let index = hoverIndex.value + offset;
if (index < 0) {
index = list.length - 1;
}
if (index >= list.length) {
index = 0;
}
hoverIndex.value = index;
};
const onKeydown = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
event.preventDefault();
changeIndex(-1);
} else if (event.key === 'ArrowDown') {
event.preventDefault();
changeIndex(1);
}
};
onMounted(() => window.addEventListener('keydown', onKeydown));
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown));
</script>
<template>
<div>
<div
:style="{
marginRight: '16px',
width: '280px',
display: 'flex',
flexWrap: 'wrap',
border: '1px solid var(--semi-color-border)',
}"
>
<List class="component-list-demo-booklist" :dataSource="list" size="small" :split="false" :style="{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }">
<template #item="{ item, index }">
<ListItem :class="{ 'component-list-demo-booklist-active-item': index === hoverIndex }">
{{ item }}
</ListItem>
</template>
</List>
</div>
</div>
</template>以上书单例子的Demo中涉及到的自定义样式如下
.component-list-demo-booklist {
.list-item {
&:hover {
background-color: var(--semi-color-fill-0);
}
&:active {
background-color: var(--semi-color-fill-1);
}
}
}
.component-list-demo-booklist-active-item {
background-color: var(--semi-color-fill-0);
}
无障碍
List 支持传入 aria-label 或 aria-labelledby 来表示列表的作用,传入后根元素带 role="group"。
设置了 @click 或 @rightClick(包括 List 上的)的 List.Item 仍是 <li> 列表项,内部渲染一个原生 <button type="button"> 作为唯一的焦点目标,读屏会读成「按钮」。
- 按钮包住
#header和#main的内容(没有这两个插槽时包住默认插槽),点击区域铺满整行;Tab 聚焦后按 Enter 或空格触发@click,上下文菜单键触发@rightClick #extra渲染在按钮外面且层级更高,其中的按钮、链接可以正常聚焦和点击,点击它们不会触发 Item 的@click- 同时设置
#header或#main和默认插槽时,默认插槽在按钮外面,会被铺满整行的按钮覆盖,其中的交互内容请放进#extra - 没有点击回调的 List.Item 不渲染按钮,也不可聚焦
List.Item 脱离 List 使用时按没有 grid 的普通列表项渲染。使用 grid 时,承载条目的 Row 带 role="list",包裹每个条目的 Col 带 role="none",读屏把条目读作这个列表的项。
API 参考
List
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| bordered | 是否显示边框。 | boolean | semiDefault("List", "bordered", false) |
| dataSource | 列表数据源;每一项通过 item 插槽渲染。 | T[] | semiGlobalDefault("List", "dataSource") |
| grid | 栅格配置:Row 的 align、gutter、justify、type 与 Col 的 span、offset、order、push、pull、响应式断点、class、style,class 与 style 落在每项外层的 Col 上。 | Grid | semiGlobalDefault("List", "grid") |
| layout | 列表布局,可选 vertical 或 horizontal。 | "vertical" | "horizontal" | semiDefault("List", "layout", "vertical") |
| loading | 是否处于加载中,为 true 时显示 Spin。 | boolean | semiDefault("List", "loading", false) |
| size | 列表尺寸,可选 small、default 或 large。 | Size | semiDefault("List", "size", "default") |
| split | 是否展示分割线。 | boolean | semiDefault("List", "split", true) |
| onClick | 列表项的点击回调,作用于每个没有自己 onClick 的项;有处理函数的项渲染为原生按钮。使用方写 @click,保留为函数 prop 是因为它的有无决定项是否渲染按钮。 | Type((event: MouseEvent) => void) | undefined |
| onRightClick | 列表项的右键(上下文菜单)回调,作用于每个没有自己 onRightClick 的项,这些项同样渲染为原生按钮。使用方写 @rightClick,保留为函数 prop 的原因同 onClick。 | Type((event: MouseEvent) => void) | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | List.Item 子节点,渲染在 dataSource 各项之后。 | any |
| item | 渲染 dataSource 中的每一项,接收 item 与 index,返回一个或多个 List.Item。 | TypeListItemSlotProps<T> |
| header | 列表头部。 | any |
| footer | 列表底部。 | any |
| emptyContent | 没有数据时的展示内容,缺省为语言包中的空文案。 | any |
| loadMore | 加载更多的按钮。 | any |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在根元素;传入 aria-label 或 aria-labelledby 且没有 role 时,根元素带 role="group"。@click、@rightClick 的载荷为原生 MouseEvent,作用于列表项而不是根元素。
List grid props
grid 只接受 Row 的 align、gutter、justify、type 与 Col 的 span、offset、order、push、pull、响应式断点、class、style,不接受其他属性;class 与 style 落在每项外层的 Col 上。参数含义请参考 Grid
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| span | 栅格占位格数 | number | - |
| gutter | 栅格间隔 | number | 0 |
| xs | <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
| sm | ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
| md | ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
| lg | ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
| xl | ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
| xxl | ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
List.Item
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | header 与 main 的垂直对齐方式,可选 flex-start、flex-end、center、baseline 或 stretch,未传时为 flex-start。 | "flex-start" | "flex-end" | "center" | "baseline" | "stretch" | undefined |
| onClick | 点击回调,优先于 List 的 onClick;有处理函数时项渲染为原生按钮。使用方写 @click,保留为函数 prop 的原因同 List。 | Type((event: MouseEvent) => void) | undefined |
| onRightClick | 右键(上下文菜单)回调,优先于 List 的 onRightClick;只设置它时项同样渲染为原生按钮。使用方写 @rightClick。 | Type((event: MouseEvent) => void) | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | 列表项内容;可点击且没有 header 与 main 插槽时放在按钮内,否则放在按钮之外。 | any |
| header | 列表项头部内容。 | any |
| main | 列表项主体内容。 | any |
| extra | 列表项附加内容,渲染在按钮外,其中的交互元素可正常聚焦与点击,点击不会触发整行的点击。 | any |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style、data-*、aria-* 与原生属性经 attrs 落在 li 上,不落在 grid 模式下外层的 Col 上。
文案规范
- 首字母大写
- 结尾不跟随标点符号
- 语法平行:如主动态与被动态、陈述句与祈使句混合使用
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $color-list_default-border-default | var(--semi-color-border) | 列表描边颜色 |
| $color-list_empty-text-default | var(--semi-color-text-2) | 空状态下列表文字颜色 |
| $spacing-list_empty-paddingX | 0px | 空状态列表水平内边距 |
| $spacing-list_empty-paddingY | $spacing-base-tight | 空状态列表垂直内边距 |
| $spacing-list_footer-paddingX | $spacing-base-tight | 列表 footer 水平内边距 |
| $spacing-list_footer-paddingY | $spacing-loose | 列表 footer 垂直内边距 |
| $spacing-list_item-paddingX | $spacing-base-tight | 列表 item 水平内边距 - 默认尺寸 |
| $spacing-list_item-paddingY | $spacing-loose | 列表 item 垂直内边距 - 默认尺寸 |
| $spacing-list_small-paddingX | $spacing-tight | 列表 item 水平内边距 - 小尺寸 |
| $spacing-list_small-paddingY | $spacing-base | 列表 item 垂直内边距 - 小尺寸 |
| $spacing-list_large-paddingX | $spacing-base | 列表 item 水平内边距 - 大尺寸 |
| $spacing-list_large-paddingY | $spacing-loose | 列表 item 垂直内边距 - 大尺寸 |
| $spacing-list_header-marginRight | $spacing-base-loose | 列表 header 右侧外边距 |
| $spacing-list_extra-marginLeft | $spacing-super-loose | 列表 extra 额外内容左侧外边距 |