el-tree 单接口滚动分页
完整组件
保存为项目内的 CustomerTreeScrollDemo.vue,使用已有的 getCustomerList 接口方法。父组件传入组织 ID;切换组织时通过 key 重建组件,重新从第一页加载:
<customer-tree-scroll-demo :key="orgId" :org-id="orgId" />
<template>
<div class="customer-tree-demo">
<!-- 监听实际滚动容器,必须有高度限制和 overflow:auto。 -->
<div ref="scrollBox" class="tree-scroll" @scroll="checkBottom">
<el-tree
ref="tree"
node-key="treeKey"
:props="treeProps"
highlight-current
@node-expand="onExpand"
@node-collapse="onCollapse"
@node-click="onClick"
/>
</div>
<div class="tree-status">
<span v-if="loading">加载中…</span>
<el-button v-else-if="failed" size="mini" @click="retry">加载失败,重试</el-button>
<span v-else-if="!hasMore">已加载全部数据</span>
<span v-else>滚动到底部加载下一页</span>
</div>
</div>
</template>
<script>
import { getCustomerList } from "@/api/business/qualityControl";
export default {
name: "CustomerTreeScrollDemo",
props: { orgId: { type: [String, Number], required: true } },
data() {
return {
pageIndex: 0,
pageSize: 200,
hasMore: true,
loading: false,
failed: false,
treeProps: { label: "name", children: "children" },
};
},
mounted() {
this.loadNextPage();
},
methods: {
parsePage(res, pageIndex) {
const payload = res && res.data;
const rawItems = Array.isArray(payload)
? payload
: (payload && (payload.dataList || payload.list || payload.items)) || [];
const items = Array.isArray(rawItems) ? rawItems : [];
// 分页信息可以放在响应顶层,也可以放在 data 对象内。
const meta = { ...res, ...(Array.isArray(payload) ? {} : payload) };
const total = meta.totalItemCount != null ? meta.totalItemCount
: meta.totalCount != null ? meta.totalCount : meta.total;
let hasMore = items.length >= this.pageSize;
if (meta.pageCount != null) hasMore = pageIndex < Number(meta.pageCount);
else if (total != null) hasMore = pageIndex * this.pageSize < Number(total);
return { items, hasMore };
},
makeItems(items, parentKey = "") {
return items.map(item => {
const id = item.iD != null ? item.iD : item.id != null ? item.id : item.code;
if (id == null || id === "") throw new Error("树节点缺少稳定的唯一 ID");
const treeKey = parentKey ? parentKey + "/" + id : String(id);
return {
...item,
treeKey,
children: Array.isArray(item.children) ? this.makeItems(item.children, treeKey) : [],
};
});
},
async loadNextPage() {
// 请求进行中、已经没有下一页,或失败等待重试时不自动请求。
if (this.loading || !this.hasMore || this.failed || this._isDestroyed) return;
const nextPage = this.pageIndex + 1;
this.loading = true;
try {
// 所有页固定调用这一个接口,不区分分类和节点类型。
const res = await getCustomerList({
orgId: this.orgId,
pageIndex: nextPage,
pageSize: this.pageSize,
});
if (this._isDestroyed) return;
const result = this.parsePage(res, nextPage);
const tree = this.$refs.tree;
const seen = new Set();
const added = this.makeItems(result.items).filter(item => {
if (seen.has(item.treeKey) || tree.getNode(item.treeKey)) return false;
seen.add(item.treeKey);
return true;
});
// 通过树组件的 append 追加,不重新赋值整棵树,保留展开和选中状态。
added.forEach(item => tree.append(item));
this.pageIndex = nextPage;
// 即使后端分页信息异常,空页或整页重复节点也不再继续请求。
this.hasMore = result.hasMore && result.items.length > 0 && added.length > 0;
} catch (error) {
if (!this._isDestroyed) this.failed = true;
// 不推进页码,等待用户重试,避免无限请求错误接口。
} finally {
this.loading = false;
if (!this._isDestroyed) this.$nextTick(this.checkBottom);
}
},
checkBottom() {
const box = this.$refs.scrollBox;
if (!box || this._isDestroyed || this.loading || this.failed || !this.hasMore) return;
const distance = box.scrollHeight - box.scrollTop - box.clientHeight;
if (distance <= 24) this.loadNextPage();
},
onExpand() {
this.$nextTick(this.checkBottom);
},
onCollapse() {
// 收起子树后内容可能不足一屏,也需要检查是否继续加载。
this.$nextTick(this.checkBottom);
},
onClick(data) {
this.$emit("select", data);
},
retry() {
this.failed = false;
this.loadNextPage();
},
},
};
</script>
<style scoped>
.customer-tree-demo {
width: 300px;
height: 500px;
display: flex;
flex-direction: column;
min-height: 0;
}
.tree-scroll {
flex: 1;
min-height: 0;
overflow: auto;
}
.tree-status {
flex-shrink: 0;
padding: 8px;
font-size: 12px;
color: #909399;
}
</style>
触底加载规则
- 组件挂载后调用
CustomerList,加载第一页。 - 滚动容器距离底部不超过 24px 时,调用同一个接口加载下一页。
loading锁住重复请求,pageIndex仅在成功后增加。tree.append追加节点,保留已加载节点、展开状态和选中状态。- 首屏数据不足以出现滚动条时,渲染后继续检查底部,自动加载到填满容器。
- 没有下一页、返回空数组或整页重复数据时停止;失败则暂停,点击重试继续原页码。
排查触底不请求
scroll必须监听真正滚动的容器。原页面使用@scroll.native监听el-tree;本示例将滚动和监听统一放在外层.tree-scroll。- 容器必须有有效高度和
overflow: auto。示例用 500px 方便验证,接入页面可换成父布局提供的高度,同时保留min-height: 0。 - 不要每次追加后把
pageIndex重置为 1,也不要替换整个树组件。 - 每条数据必须有稳定的唯一 ID,接口分页应稳定排序。示例通过
treeKey去重,子树节点增加父节点前缀。 - 如果接口只返回完整数组、没有分页字段,按每页返回数量判断是否继续;后端重复返回同一页时,示例会在整页重复数据后停止。

浙公网安备 33010602011771号