通过scroll监听和虚拟列表技术实现,完整代码如下:
<template> <view class="virtual-list"> <scroll-view class="scroll-view" scroll-y :scroll-top="scrollTop" :style="{ height: height + 'px' }" @scroll="onScroll" :scroll-with-animation="false" > <view class="loading" v-if="isLoading">加载中...</view> <view class="list-content" :style="{ height: totalHeight + 'px' }"> <view v-for="item in visibleData" :key="item.id" class="list-item" :style="{ transform: `translateY(${item.top}px)`, position: 'absolute', width: '100%' }" > <view :class="['message', item.isSelf ? 'self' : 'other']"> <view class="avatar">{{item.isSelf ? '我' : '对方'}}</view> <view class="content">{{item.content}}</view> </view> </view> </view> </scroll-view> </view> </template> <script> export default { data() { return { height: 0, itemHeight: 80, visibleCount: 0, bufferCount: 5, listData: [], visibleData: [], isLoading: false, scrollTop: 0, startIndex: 0, loadMoreThreshold: 500, lastLoadTime: 0, loadCooldown: 1000, totalHeight: 0, lastScrollTop: 0, scrollTimeout: null } }, async mounted() { const sysInfo = uni.getSystemInfoSync() this.height = sysInfo.windowHeight this.visibleCount = Math.ceil(this.height / this.itemHeight) + this.bufferCount * 2 await this.initData() }, methods: { async initData() { const initList = Array(30).fill(0).map((_, index) => ({ id: index, content: `Message ${index}`, isSelf: Math.random() > 0.5, top: index * this.itemHeight })) this.listData = initList this.totalHeight = this.listData.length * this.itemHeight this.updateVisibleData() }, updateVisibleData() { const scrollTop = this.scrollTop const start = Math.floor(scrollTop / this.itemHeight) - this.bufferCount const end = start + this.visibleCount + this.bufferCount * 2 const startIndex = Math.max(0, start) const endIndex = Math.min(this.listData.length, end) this.visibleData = this.listData.slice(startIndex, endIndex).map(item => ({ ...item, top: item.top })) }, onScroll(e) { const newScrollTop = e.detail.scrollTop this.lastScrollTop = this.scrollTop this.scrollTop = newScrollTop // 使用防抖处理滚动更新 if(this.scrollTimeout) { clearTimeout(this.scrollTimeout) } this.scrollTimeout = setTimeout(() => { this.updateVisibleData() this.checkNeedLoadMore(newScrollTop) }, 300) // 约等于一帧的时间 }, async checkNeedLoadMore(scrollTop) { if(scrollTop < this.loadMoreThreshold) { const now = Date.now() if(now - this.lastLoadTime > this.loadCooldown) { await this.loadMore() } } }, async loadMore() { if(this.isLoading) return this.isLoading = true this.lastLoadTime = Date.now() try { const currentFirstItem = this.visibleData[0] const oldOffset = currentFirstItem ? currentFirstItem.top : 0 await new Promise(resolve => setTimeout(resolve, 800)) const newMessages = Array(20).fill(0).map((_, index) => ({ id: `new_${this.listData.length + index}`, content: `History Message ${this.listData.length + index}`, isSelf: Math.random() > 0.5 })) // 计算新消息的位置 const heightOffset = newMessages.length * this.itemHeight // 更新现有消息的位置 this.listData = this.listData.map(item => ({ ...item, top: item.top + heightOffset })) // 添加新消息 const newMessagesWithPosition = newMessages.map((item, index) => ({ ...item, top: index * this.itemHeight })) // 合并数据 this.listData = [...newMessagesWithPosition, ...this.listData] // 更新总高度 this.totalHeight = this.listData.length * this.itemHeight // 调整滚动位置 const newScrollTop = oldOffset + heightOffset this.scrollTop = newScrollTop await this.$nextTick() this.updateVisibleData() } finally { this.isLoading = false } } } } </script> <style lang="scss"> .virtual-list { width: 100%; .scroll-view { width: 100%; } .list-content { position: relative; width: 100%; } .loading { position: absolute; top: 0; left: 0; right: 0; text-align: center; padding: 10px; color: #999; z-index: 1; } .list-item { padding: 10px; box-sizing: border-box; .message { display: flex; align-items: flex-start; margin: 10px 0; &.self { flex-direction: row-reverse; .content { margin: 0 10px 0 50px; background: #007AFF; color: #fff; } } &.other { .content { margin: 0 50px 0 10px; background: #f0f0f0; } } .avatar { width: 40px; height: 40px; border-radius: 50%; background: #ddd; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .content { padding: 10px; border-radius: 4px; word-break: break-all; } } } } </style>
网友回复