123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- <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>
|