253 lines
6.3 KiB
Vue
253 lines
6.3 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="u-virtual-list" :style="{ height: addUnit(height) }" ref="container">
|
|||
|
|
<scroll-view
|
|||
|
|
class="virtual-scroll-container"
|
|||
|
|
:scroll-y="true"
|
|||
|
|
:scroll-top="scrollTop"
|
|||
|
|
:style="{ height: '100%' }"
|
|||
|
|
@scroll="handleScroll"
|
|||
|
|
>
|
|||
|
|
<!-- @touchmove.stop.prevent="handleTouchMove" -->
|
|||
|
|
<view class="scroll-content">
|
|||
|
|
<!-- 顶部占位 -->
|
|||
|
|
<view :style="{ height: topPlaceholderHeight + 'px' }"></view>
|
|||
|
|
|
|||
|
|
<!-- 可见项 -->
|
|||
|
|
<view
|
|||
|
|
v-for="item in visibleItems"
|
|||
|
|
:key="getItemKey(item)"
|
|||
|
|
class="list-item"
|
|||
|
|
:style="{ height: itemHeight + 'px' }"
|
|||
|
|
>
|
|||
|
|
<slot :item="item" :index="item._virtualIndex"></slot>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 底部占位 -->
|
|||
|
|
<view :style="{ height: bottomPlaceholderHeight + 'px' }"></view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { addUnit } from '../../libs/function/index.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'u-virtual-list',
|
|||
|
|
props: {
|
|||
|
|
// 数据源
|
|||
|
|
listData: {
|
|||
|
|
type: Array,
|
|||
|
|
default: () => []
|
|||
|
|
},
|
|||
|
|
// 每项高度(固定高度模式)
|
|||
|
|
itemHeight: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 50
|
|||
|
|
},
|
|||
|
|
// 容器高度
|
|||
|
|
height: {
|
|||
|
|
type: [String, Number],
|
|||
|
|
default: '100%'
|
|||
|
|
},
|
|||
|
|
// 缓冲区项数
|
|||
|
|
buffer: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 4
|
|||
|
|
},
|
|||
|
|
// 索引键名
|
|||
|
|
keyField: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'id'
|
|||
|
|
},
|
|||
|
|
// 当前滚动位置
|
|||
|
|
scrollTop: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
// 起始索引
|
|||
|
|
startIndex: 0,
|
|||
|
|
// 容器实际高度
|
|||
|
|
containerHeight: 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
// 可视区域显示的项数(根据容器实际高度自动计算)
|
|||
|
|
remain() {
|
|||
|
|
if (this.containerHeight <= 0) {
|
|||
|
|
// 默认值,防止除以0
|
|||
|
|
return Math.ceil(500 / this.itemHeight) || 10
|
|||
|
|
}
|
|||
|
|
const calculated = Math.ceil(this.containerHeight / this.itemHeight)
|
|||
|
|
// 确保至少显示一些项
|
|||
|
|
return Math.max(1, calculated)
|
|||
|
|
},
|
|||
|
|
// 可视项数量
|
|||
|
|
visibleCount() {
|
|||
|
|
return this.remain + this.buffer
|
|||
|
|
},
|
|||
|
|
// 可视项
|
|||
|
|
visibleItems() {
|
|||
|
|
const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
|
|||
|
|
const end = Math.min(this.listData.length, start + this.visibleCount)
|
|||
|
|
|
|||
|
|
return this.listData.slice(start, end).map((item, index) => {
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
_virtualIndex: start + index
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 顶部占位高度
|
|||
|
|
topPlaceholderHeight() {
|
|||
|
|
const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
|
|||
|
|
return start * this.itemHeight
|
|||
|
|
},
|
|||
|
|
// 底部占位高度
|
|||
|
|
bottomPlaceholderHeight() {
|
|||
|
|
const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
|
|||
|
|
const end = Math.min(this.listData.length, start + this.visibleCount)
|
|||
|
|
return (this.listData.length - end) * this.itemHeight
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
emits: ['update:scrollTop', 'scroll'],
|
|||
|
|
watch: {
|
|||
|
|
listData: {
|
|||
|
|
handler() {
|
|||
|
|
this.updateVisibleItems()
|
|||
|
|
},
|
|||
|
|
immediate: true
|
|||
|
|
},
|
|||
|
|
scrollTop: {
|
|||
|
|
handler(newVal) {
|
|||
|
|
this.updateVisibleItems()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
mounted() {
|
|||
|
|
this.measureContainerHeight()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
addUnit,
|
|||
|
|
|
|||
|
|
// 测量容器高度
|
|||
|
|
measureContainerHeight() {
|
|||
|
|
// 使用 uni.createSelectorQuery 获取实际高度
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
// #ifdef H5
|
|||
|
|
if (this.$refs.container) {
|
|||
|
|
const element = this.$refs.container.$el || this.$refs.container
|
|||
|
|
this.containerHeight = element.offsetHeight || 500
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef H5
|
|||
|
|
const query = uni.createSelectorQuery().in(this)
|
|||
|
|
query.select('.u-virtual-list').boundingClientRect(rect => {
|
|||
|
|
if (rect) {
|
|||
|
|
this.containerHeight = rect.height || 500
|
|||
|
|
} else {
|
|||
|
|
// 如果无法获取实际高度,使用默认计算
|
|||
|
|
this.containerHeight = this.calculateDefaultHeight()
|
|||
|
|
}
|
|||
|
|
}).exec()
|
|||
|
|
// #endif
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 计算默认高度
|
|||
|
|
calculateDefaultHeight() {
|
|||
|
|
const height = this.height
|
|||
|
|
if (typeof height === 'number') {
|
|||
|
|
return height
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (typeof height === 'string') {
|
|||
|
|
if (height.includes('px')) {
|
|||
|
|
return parseInt(height) || 500
|
|||
|
|
} else if (height.includes('vh')) {
|
|||
|
|
// 处理视口高度单位
|
|||
|
|
const vh = parseInt(height)
|
|||
|
|
return isNaN(vh) ? 500 : (vh / 100) * this.getViewportHeight()
|
|||
|
|
} else if (height.includes('%')) {
|
|||
|
|
// 百分比高度,使用默认值
|
|||
|
|
return 500
|
|||
|
|
} else {
|
|||
|
|
const num = parseInt(height)
|
|||
|
|
return isNaN(num) ? 500 : num
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 500
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取视口高度
|
|||
|
|
getViewportHeight() {
|
|||
|
|
// #ifdef H5
|
|||
|
|
return window.innerHeight
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef H5
|
|||
|
|
try {
|
|||
|
|
const res = uni.getSystemInfoSync()
|
|||
|
|
return res.windowHeight
|
|||
|
|
} catch (e) {
|
|||
|
|
return 600 // 默认值
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getItemKey(item) {
|
|||
|
|
return item[this.keyField] !== undefined ? item[this.keyField] : item._virtualIndex
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 更新可视项
|
|||
|
|
updateVisibleItems() {
|
|||
|
|
const index = Math.floor(this.scrollTop / this.itemHeight)
|
|||
|
|
this.startIndex = Math.max(0, index)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 处理滚动
|
|||
|
|
handleScroll(e) {
|
|||
|
|
const scrollTop = e.detail.scrollTop
|
|||
|
|
this.$emit('update:scrollTop', scrollTop)
|
|||
|
|
this.$emit('scroll', scrollTop)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 处理触摸移动,阻止事件冒泡
|
|||
|
|
handleTouchMove(e) {
|
|||
|
|
// 阻止触摸移动事件冒泡到父级,防止触发页面滚动
|
|||
|
|
e.stopPropagation()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取可见项范围
|
|||
|
|
getVisibleRange() {
|
|||
|
|
const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
|
|||
|
|
const end = Math.min(this.listData.length, start + this.visibleCount)
|
|||
|
|
return { start, end }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.u-virtual-list {
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.virtual-scroll-container {
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.scroll-content {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.list-item {
|
|||
|
|
will-change: transform;
|
|||
|
|
}
|
|||
|
|
</style>
|