展示类

列表

基础列表组件

代码演示

如何引入

<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

Header
  • 从明天起,做一个幸福的人
  • 喂马,劈柴,周游世界
  • 从明天起,关心粮食和蔬菜
  • 我有一所房子,面朝大海,春暖花开

Small Size

Header
  • 从明天起,做一个幸福的人
  • 喂马,劈柴,周游世界
  • 从明天起,关心粮食和蔬菜
  • 我有一所房子,面朝大海,春暖花开

Large Size

Header
  • 从明天起,做一个幸福的人
  • 喂马,劈柴,周游世界
  • 从明天起,关心粮食和蔬菜
  • 我有一所房子,面朝大海,春暖花开
代码
<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
    示例标题

    Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • 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。

  • SE
    Semi Design Title 1

    Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • SE
    Semi Design Title 2

    Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

  • SE
    Semi 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 才能把节点对应到正确的数据。

    • SE
      Semi Design Title 1

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 2

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 3

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi Design Title 4

      Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。

    • SE
      Semi 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是否显示边框。booleansemiDefault("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 上。GridsemiGlobalDefault("List", "grid")
    layout列表布局,可选 vertical 或 horizontal。"vertical" | "horizontal"semiDefault("List", "layout", "vertical")
    loading是否处于加载中,为 true 时显示 Spin。booleansemiDefault("List", "loading", false)
    size列表尺寸,可选 small、default 或 large。SizesemiDefault("List", "size", "default")
    split是否展示分割线。booleansemiDefault("List", "split", true)
    onClick列表项的点击回调,作用于每个没有自己 onClick 的项;有处理函数的项渲染为原生按钮。使用方写 @click,保留为函数 prop 是因为它的有无决定项是否渲染按钮。Type((event: MouseEvent) => void)undefined
    onRightClick列表项的右键(上下文菜单)回调,作用于每个没有自己 onRightClick 的项,这些项同样渲染为原生按钮。使用方写 @rightClick,保留为函数 prop 的原因同 onClick。Type((event: MouseEvent) => void)undefined

    Events

    事件说明回调参数
    无

    Slots

    插槽说明作用域参数
    defaultList.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栅格间隔number0
    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

    属性说明类型默认值
    alignheader 与 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 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
    Sass 变量值用法
    $color-list_default-border-defaultvar(--semi-color-border)列表描边颜色
    $color-list_empty-text-defaultvar(--semi-color-text-2)空状态下列表文字颜色
    $spacing-list_empty-paddingX0px空状态列表水平内边距
    $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 额外内容左侧外边距