u-virtual-list.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <template>
  2. <view class="u-virtual-list" :style="{ height: addUnit(height) }" ref="container">
  3. <scroll-view
  4. class="virtual-scroll-container"
  5. :scroll-y="true"
  6. :scroll-top="scrollTop"
  7. :style="{ height: '100%' }"
  8. @scroll="handleScroll"
  9. >
  10. <!-- @touchmove.stop.prevent="handleTouchMove" -->
  11. <view class="scroll-content">
  12. <!-- 顶部占位 -->
  13. <view :style="{ height: topPlaceholderHeight + 'px' }"></view>
  14. <!-- 可见项 -->
  15. <view
  16. v-for="item in visibleItems"
  17. :key="getItemKey(item)"
  18. class="list-item"
  19. :style="{ height: itemHeight + 'px' }"
  20. >
  21. <slot :item="item" :index="item._virtualIndex"></slot>
  22. </view>
  23. <!-- 底部占位 -->
  24. <view :style="{ height: bottomPlaceholderHeight + 'px' }"></view>
  25. </view>
  26. </scroll-view>
  27. </view>
  28. </template>
  29. <script>
  30. import { addUnit } from '../../libs/function/index.js'
  31. export default {
  32. name: 'u-virtual-list',
  33. props: {
  34. // 数据源
  35. listData: {
  36. type: Array,
  37. default: () => []
  38. },
  39. // 每项高度(固定高度模式)
  40. itemHeight: {
  41. type: Number,
  42. default: 50
  43. },
  44. // 容器高度
  45. height: {
  46. type: [String, Number],
  47. default: '100%'
  48. },
  49. // 缓冲区项数
  50. buffer: {
  51. type: Number,
  52. default: 4
  53. },
  54. // 索引键名
  55. keyField: {
  56. type: String,
  57. default: 'id'
  58. },
  59. // 当前滚动位置
  60. scrollTop: {
  61. type: Number,
  62. default: 0
  63. }
  64. },
  65. data() {
  66. return {
  67. // 起始索引
  68. startIndex: 0,
  69. // 容器实际高度
  70. containerHeight: 0
  71. }
  72. },
  73. computed: {
  74. // 可视区域显示的项数(根据容器实际高度自动计算)
  75. remain() {
  76. if (this.containerHeight <= 0) {
  77. // 默认值,防止除以0
  78. return Math.ceil(500 / this.itemHeight) || 10
  79. }
  80. const calculated = Math.ceil(this.containerHeight / this.itemHeight)
  81. // 确保至少显示一些项
  82. return Math.max(1, calculated)
  83. },
  84. // 可视项数量
  85. visibleCount() {
  86. return this.remain + this.buffer
  87. },
  88. // 可视项
  89. visibleItems() {
  90. const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
  91. const end = Math.min(this.listData.length, start + this.visibleCount)
  92. return this.listData.slice(start, end).map((item, index) => {
  93. return {
  94. ...item,
  95. _virtualIndex: start + index
  96. }
  97. })
  98. },
  99. // 顶部占位高度
  100. topPlaceholderHeight() {
  101. const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
  102. return start * this.itemHeight
  103. },
  104. // 底部占位高度
  105. bottomPlaceholderHeight() {
  106. const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
  107. const end = Math.min(this.listData.length, start + this.visibleCount)
  108. return (this.listData.length - end) * this.itemHeight
  109. }
  110. },
  111. emits: ['update:scrollTop', 'scroll'],
  112. watch: {
  113. listData: {
  114. handler() {
  115. this.updateVisibleItems()
  116. },
  117. immediate: true
  118. },
  119. scrollTop: {
  120. handler(newVal) {
  121. this.updateVisibleItems()
  122. }
  123. }
  124. },
  125. mounted() {
  126. this.measureContainerHeight()
  127. },
  128. methods: {
  129. addUnit,
  130. // 测量容器高度
  131. measureContainerHeight() {
  132. // 使用 uni.createSelectorQuery 获取实际高度
  133. this.$nextTick(() => {
  134. // #ifdef H5
  135. if (this.$refs.container) {
  136. const element = this.$refs.container.$el || this.$refs.container
  137. this.containerHeight = element.offsetHeight || 500
  138. }
  139. // #endif
  140. // #ifndef H5
  141. const query = uni.createSelectorQuery().in(this)
  142. query.select('.u-virtual-list').boundingClientRect(rect => {
  143. if (rect) {
  144. this.containerHeight = rect.height || 500
  145. } else {
  146. // 如果无法获取实际高度,使用默认计算
  147. this.containerHeight = this.calculateDefaultHeight()
  148. }
  149. }).exec()
  150. // #endif
  151. })
  152. },
  153. // 计算默认高度
  154. calculateDefaultHeight() {
  155. const height = this.height
  156. if (typeof height === 'number') {
  157. return height
  158. }
  159. if (typeof height === 'string') {
  160. if (height.includes('px')) {
  161. return parseInt(height) || 500
  162. } else if (height.includes('vh')) {
  163. // 处理视口高度单位
  164. const vh = parseInt(height)
  165. return isNaN(vh) ? 500 : (vh / 100) * this.getViewportHeight()
  166. } else if (height.includes('%')) {
  167. // 百分比高度,使用默认值
  168. return 500
  169. } else {
  170. const num = parseInt(height)
  171. return isNaN(num) ? 500 : num
  172. }
  173. }
  174. return 500
  175. },
  176. // 获取视口高度
  177. getViewportHeight() {
  178. // #ifdef H5
  179. return window.innerHeight
  180. // #endif
  181. // #ifndef H5
  182. try {
  183. const res = uni.getSystemInfoSync()
  184. return res.windowHeight
  185. } catch (e) {
  186. return 600 // 默认值
  187. }
  188. // #endif
  189. },
  190. getItemKey(item) {
  191. return item[this.keyField] !== undefined ? item[this.keyField] : item._virtualIndex
  192. },
  193. // 更新可视项
  194. updateVisibleItems() {
  195. const index = Math.floor(this.scrollTop / this.itemHeight)
  196. this.startIndex = Math.max(0, index)
  197. },
  198. // 处理滚动
  199. handleScroll(e) {
  200. const scrollTop = e.detail.scrollTop
  201. this.$emit('update:scrollTop', scrollTop)
  202. this.$emit('scroll', scrollTop)
  203. },
  204. // 处理触摸移动,阻止事件冒泡
  205. handleTouchMove(e) {
  206. // 阻止触摸移动事件冒泡到父级,防止触发页面滚动
  207. e.stopPropagation()
  208. },
  209. // 获取可见项范围
  210. getVisibleRange() {
  211. const start = Math.max(0, this.startIndex - Math.floor(this.buffer / 2))
  212. const end = Math.min(this.listData.length, start + this.visibleCount)
  213. return { start, end }
  214. }
  215. }
  216. }
  217. </script>
  218. <style scoped lang="scss">
  219. .u-virtual-list {
  220. position: relative;
  221. overflow: hidden;
  222. }
  223. .virtual-scroll-container {
  224. height: 100%;
  225. }
  226. .scroll-content {
  227. position: relative;
  228. }
  229. .list-item {
  230. will-change: transform;
  231. }
  232. </style>