Appearance
BizInfiniteList 无限滚动列表
滚动到底自动加载下一页的列表容器:request 注入按页拉取(组件零接口),内置加载中 / 失败重试 / 没有更多三态与空态兜底,首屏不满自动续拉。适用于消息中心、操作日志流、审批待办、feed 流等「往下刷」型页面。
基础用法
滚动到距底 60px 自动加载下一页;
hasMore 缺省时按「满页 + total」推断。<script setup lang="ts">
/** 基础用法:滚动到底自动加载下一页(mock 87 条消息) */
import { BizInfiniteList } from 'vue3-ep-biz-lib'
interface Message {
id: number
title: string
time: string
}
const TOTAL = 87
const PAGE_SIZE = 20
const request = async ({ page, pageSize }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 400))
const start = (page - 1) * pageSize
const list: Message[] = Array.from({ length: Math.min(pageSize, TOTAL - start) }, (_, i) => ({
id: start + i + 1,
title: `系统消息:第 ${start + i + 1} 条通知内容摘要`,
time: `2026-09-${String((start + i) % 28 + 1).padStart(2, '0')} 14:30`,
}))
return { list, hasMore: start + list.length < TOTAL, total: TOTAL }
}
</script>
<template>
<BizInfiniteList :request="request" :page-size="PAGE_SIZE" :height="360">
<template #item="{ item }">
<div class="msg">
<span class="title">{{ item.title }}</span>
<span class="time">{{ item.time }}</span>
</div>
</template>
</BizInfiniteList>
</template>
<style scoped>
.msg {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.msg:hover {
background: var(--el-fill-color-light);
}
.title {
flex: 1;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time {
flex-shrink: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
失败重试 · 编程式重载
第 2 页 mock 失败演示「点击重试」;
reset() 清空重拉(常用于筛选条件变化后);finished 插槽自定义完结文案。<script setup lang="ts">
/** 进阶:失败重试 + reset 编程式重载 + loaded/error 事件 */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizInfiniteList } from 'vue3-ep-biz-lib'
import type { BizInfiniteListInstance } from 'vue3-ep-biz-lib'
const listRef = ref<BizInfiniteListInstance<{ id: number; text: string }> | null>(null)
let failCount = 0
const request = async ({ page }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 300))
failCount += 1
// 第 2 页模拟失败,演示「点击重试」
if (page === 2 && failCount % 2 === 0) {
throw new Error('network error')
}
const size = 15
const list = Array.from({ length: size }, (_, i) => ({
id: (page - 1) * size + i + 1,
text: `操作日志 #${(page - 1) * size + i + 1}`,
}))
return { list, hasMore: page < 5 }
}
const onError = (): void => {
ElMessage.error('加载失败(mock),点击列表底部可重试')
}
const reload = (): void => {
listRef.value?.reset()
}
</script>
<template>
<div>
<div style="margin-bottom: 8px">
<ElButton size="small" @click="reload">reset() 重新加载</ElButton>
</div>
<BizInfiniteList ref="listRef" :request="request" :height="320" :page-size="15" @error="onError">
<template #item="{ item }">
<div class="log">{{ item.text }} · 2026-09-29 10:00:00</div>
</template>
<template #finished>
<span style="color: var(--el-color-success)">✔ 全部加载完成</span>
</template>
</BizInfiniteList>
</div>
</template>
<style scoped>
.log {
padding: 8px 14px;
font-size: 13px;
color: var(--el-text-color-regular);
border-bottom: 1px dashed var(--el-border-color-lighter);
}
</style>
显示代码
筛选条件变化重载
header 槽放筛选条,条件切换 reset() 清空重拉(列表内嵌筛选场景)。
<script setup lang="ts">
/** header 槽筛选条 + reset() 重载:条件变化即清空重拉 */
import { ref } from 'vue'
import { BizInfiniteList } from 'vue3-ep-biz-lib'
import type { BizInfiniteListInstance } from 'vue3-ep-biz-lib'
const level = ref<'all' | 'warn' | 'error'>('all')
const listRef = ref<BizInfiniteListInstance | null>(null)
const ALL = Array.from({ length: 60 }, (_, i) => ({
id: i + 1,
level: (['info', 'warn', 'error'] as const)[i % 3],
text: `日志事件 #${i + 1}`,
}))
const request = async ({ page, pageSize }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 300))
const filtered = level.value === 'all' ? ALL : ALL.filter((log) => log.level === level.value)
const list = filtered.slice((page - 1) * pageSize, page * pageSize)
return { list, hasMore: (page) * pageSize < filtered.length, total: filtered.length }
}
const onLevel = (value: 'all' | 'warn' | 'error'): void => {
level.value = value
listRef.value?.reset()
}
</script>
<template>
<BizInfiniteList ref="listRef" :request="request" :page-size="10" :height="300">
<template #header>
<div class="filter">
<ElRadioGroup :model-value="level" size="small" @update:model-value="onLevel">
<ElRadioButton value="all">全部</ElRadioButton>
<ElRadioButton value="warn">警告</ElRadioButton>
<ElRadioButton value="error">错误</ElRadioButton>
</ElRadioGroup>
</div>
</template>
<template #item="{ item }">
<div class="log">
<ElTag size="small" :type="item.level === 'error' ? 'danger' : item.level === 'warn' ? 'warning' : 'info'">
{{ item.level }}
</ElTag>
<span>{{ item.text }}</span>
</div>
</template>
</BizInfiniteList>
</template>
<style scoped>
.filter {
padding-bottom: 10px;
}
.log {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 14px;
font-size: 13px;
border-bottom: 1px dashed var(--el-border-color-lighter);
}
</style>
显示代码
空态/完结自定义
empty 与 finished 插槽整体自定义;可切换空数据演示。
<script setup lang="ts">
/** 纯展示会话记录:show-input 不适用(本组件无输入区)——empty/finished 插槽全自定义 */
import { ref } from 'vue'
import { BizInfiniteList } from 'vue3-ep-biz-lib'
const EMPTY = ref(false)
let loaded = 0
const request = async ({ page, pageSize }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 400))
if (EMPTY.value) return { list: [], hasMore: false, total: 0 }
loaded += 1
const list = Array.from({ length: Math.min(pageSize, 20 - (page - 1) * pageSize) }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
text: `历史会话记录 ${(page - 1) * pageSize + i + 1}`,
}))
return { list, hasMore: page * pageSize < 20, total: 20 }
}
const toggle = (): void => {
EMPTY.value = !EMPTY.value
loaded = 0
}
void loaded
</script>
<template>
<div>
<BizInfiniteList :request="request" :page-size="8" :height="260">
<template #item="{ item }">
<div class="rec">
<ElIcon><ChatLineSquare /></ElIcon>
{{ item.text }}
</div>
</template>
<template #finished>
<span style="color: var(--el-color-success)">✔ 已加载全部 20 条(finished 插槽自定义)</span>
</template>
<template #empty>
<div class="empty">
<ElIcon :size="28"><Box /></ElIcon>
<p>暂无会话记录(empty 插槽整体自定义)</p>
</div>
</template>
</BizInfiniteList>
<ElButton size="small" style="margin-top: 8px" @click="toggle">{{ EMPTY ? '恢复数据' : '置空数据' }}</ElButton>
</div>
</template>
<style scoped>
.rec {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
font-size: 13px;
border-bottom: 1px dashed var(--el-border-color-lighter);
}
.empty {
padding: 32px 0;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style>
显示代码
手动起拉
数据较重,确认后加载(immediate=false)
immediate=false 先确认再 loadMore(),重数据 / 按需加载场景。
<script setup lang="ts">
/** immediate=false:先确认条件再手动 loadMore 起拉 */
import { ref } from 'vue'
import { BizInfiniteList } from 'vue3-ep-biz-lib'
import type { BizInfiniteListInstance } from 'vue3-ep-biz-lib'
const listRef = ref<BizInfiniteListInstance | null>(null)
const started = ref(false)
const request = async ({ page, pageSize }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 300))
const list = Array.from({ length: Math.min(pageSize, 45 - (page - 1) * pageSize) }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
text: `待办事项 ${(page - 1) * pageSize + i + 1}`,
}))
return { list, hasMore: page * pageSize < 45, total: 45 }
}
const start = (): void => {
started.value = true
void listRef.value?.loadMore()
}
</script>
<template>
<div>
<ElAlert v-if="!started" type="info" :closable="false" title="数据较重,确认后加载(immediate=false)" />
<ElButton v-if="!started" size="small" style="margin: 8px 0" type="primary" @click="start">开始加载</ElButton>
<BizInfiniteList
v-if="started"
ref="listRef"
:request="request"
:immediate="false"
:page-size="10"
:height="260"
>
<template #item="{ item }">
<div class="todo">{{ item.text }}</div>
</template>
</BizInfiniteList>
</div>
</template>
<style scoped>
.todo {
padding: 8px 14px;
font-size: 13px;
border-bottom: 1px dashed var(--el-border-color-lighter);
}
</style>
显示代码
不等高 feed 流
动态卡片高度不均验证滚动加载;上翻阅读暂停沉底 + 悬浮回底按钮。
<script setup lang="ts">
/** 动态 feed 流:不等高卡片 + 上翻暂停沉底(回底按钮演示) */
import { BizInfiniteList } from 'vue3-ep-biz-lib'
const request = async ({ page }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 500))
const list = Array.from({ length: 6 }, (_, i) => ({
id: (page - 1) * 6 + i + 1,
author: ['产品周报', '运营日报', '技术分享', '设计走查'][i % 4],
lines: 2 + ((page + i) % 4),
}))
return { list, hasMore: page < 6, total: 36 }
}
</script>
<template>
<BizInfiniteList :request="request" :page-size="6" :height="320" finished-text="— 已到底,看看往期吧 —">
<template #item="{ item }">
<div class="feed">
<div class="head">
<ElAvatar :size="24">{{ item.author.slice(0, 1) }}</ElAvatar>
<strong>{{ item.author }}</strong>
<span class="no">#{{ item.id }}</span>
</div>
<p class="body" :style="{ '--lines': item.lines }">
内容摘要行一;内容摘要行二;内容摘要行三;内容摘要行四(行数不等的卡片验证不等高滚动)。
</p>
</div>
</template>
</BizInfiniteList>
</template>
<style scoped>
.feed {
margin: 0 14px 12px;
padding: 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: var(--el-border-radius-base);
}
.head {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.no {
margin-left: auto;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.body {
margin: 8px 0 0;
font-size: 13px;
color: var(--el-text-color-regular);
line-height: 1.7;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines);
overflow: hidden;
}
</style>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| request | (params: { page, pageSize }) => Promise<{ list, hasMore?, total? }> | — | 必填。分页数据源(业务接口在业务侧) |
| page-size | number | 20 | 分页大小 |
| immediate | boolean | true | 挂载后立即加载第一页 |
| threshold | number | 60 | 距底触发距离(px) |
| height | number | 420 | 滚动容器高度(px) |
| loading-text | string | '加载中…' | 加载中文案(空串仅图标) |
| finished-text | string | '没有更多了' | 完结文案(空串隐藏) |
| error-text | string | '加载失败,点击重试' | 失败提示文案 |
| empty-text | string | '暂无数据' | 空态文案 |
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| loaded | (payload: { page, list, total }) | 一页加载完成 |
| error | (err: unknown) | 本次加载失败 |
Slots
| 插槽名 | 说明 |
|---|---|
| item | 行内容(作用域:item、index) |
| header | 列表顶部内容(如筛选条) |
| finished | 完结文案区自定义 |
| empty | 空态整体自定义(作用域:description,缺省 BizEmpty) |
Expose
| 属性/方法 | 说明 |
|---|---|
| loadMore | () => Promise<void> 手动加载下一页 |
| reset | () => void 清空重拉第一页 |
| items / page / total / finished | 已加载数据 / 页码 / 总数 / 完结标记 |