AI_Painting_V2.0/src/components/virtual-scroller/useVirtualScroller.js

247 lines
5.8 KiB
JavaScript

import { ref, computed, watch, nextTick } from 'vue'
export default function useVirtualScroller(props, emit, scrollerRef) {
const heights = ref(new Map())
const positions = ref([])
const scrollTop = ref(0)
const clientHeight = ref(0)
const isLoading = ref(false)
let lastScrollTime = 0
let animationFrameId = null
const savedScrollPosition = ref(null)
const getHeight = (index) => {
return heights.value.get(index) || props.estimatedHeight
}
const initPositions = () => {
const data = props.data || []
positions.value = []
let top = 0
for (let i = 0; i < data.length; i++) {
const height = getHeight(i)
positions.value.push({
index: i,
top,
bottom: top + height,
height
})
top += height
}
}
const totalHeight = computed(() => {
if (positions.value.length === 0) return 0
return positions.value[positions.value.length - 1].bottom
})
const getVisibleRange = () => {
const data = props.data || []
if (data.length === 0) return { start: 0, end: 0 }
let start = 0
let end = data.length
const currentScrollTop = scrollTop.value
const currentClientHeight = clientHeight.value
for (let i = 0; i < positions.value.length; i++) {
if (positions.value[i].bottom > currentScrollTop) {
start = i
break
}
}
for (let i = start; i < positions.value.length; i++) {
if (positions.value[i].top > currentScrollTop + currentClientHeight) {
end = i
break
}
}
start = Math.max(0, start - props.buffer)
end = Math.min(data.length, end + props.buffer)
return { start, end }
}
const visibleData = computed(() => {
const { start, end } = getVisibleRange()
const data = props.data || []
const result = []
for (let i = start; i < end; i++) {
result.push({
item: data[i],
index: i,
top: positions.value[i]?.top || 0
})
}
return result
})
const handleScroll = (event) => {
const now = Date.now()
const throttleTime = 16
if (now - lastScrollTime < throttleTime) return
lastScrollTime = now
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
animationFrameId = requestAnimationFrame(() => {
const target = event.target
scrollTop.value = target.scrollTop
clientHeight.value = target.clientHeight
checkLoadMore(target)
})
}
const checkLoadMore = (target) => {
if (isLoading.value || !props.onLoadMore) return
const { scrollTop: st, scrollHeight, clientHeight: ch } = target
const distanceToTop = st
const distanceToBottom = scrollHeight - st - ch
if (props.renderMode === 'bottom') {
if (distanceToTop <= props.scrollThreshold) {
loadMore()
}
} else {
if (distanceToBottom <= props.scrollThreshold) {
loadMore()
}
}
}
const loadMore = async () => {
if (isLoading.value || !props.onLoadMore) return
isLoading.value = true
emit('load-more')
try {
await props.onLoadMore()
} finally {
isLoading.value = false
}
}
const scrollToBottom = () => {
nextTick(() => {
if (scrollerRef.value) {
scrollerRef.value.scrollTop = scrollerRef.value.scrollHeight
}
})
}
const scrollToTop = () => {
nextTick(() => {
if (scrollerRef.value) {
scrollerRef.value.scrollTop = 0
}
})
}
const scrollToIndex = (index) => {
nextTick(() => {
if (scrollerRef.value && positions.value[index]) {
scrollerRef.value.scrollTop = positions.value[index].top
}
})
}
const updateData = (newData) => {
initPositions()
}
const getScrollPosition = () => {
if (scrollerRef.value) {
return scrollerRef.value.scrollTop
}
return 0
}
const setScrollPosition = (position) => {
nextTick(() => {
if (scrollerRef.value) {
scrollerRef.value.scrollTop = position
}
})
}
const updateItemHeight = (index, height) => {
if (!props.cacheHeight) return
const oldHeight = heights.value.get(index) || props.estimatedHeight
if (oldHeight === height) return
heights.value.set(index, height)
const oldScrollTop = scrollerRef.value?.scrollTop || 0
const oldScrollHeight = scrollerRef.value?.scrollHeight || 0
initPositions()
nextTick(() => {
if (scrollerRef.value && props.renderMode === 'bottom') {
const newScrollHeight = scrollerRef.value.scrollHeight
const heightDiff = newScrollHeight - oldScrollHeight
if (heightDiff > 0) {
scrollerRef.value.scrollTop = oldScrollTop + heightDiff
}
}
})
}
const clearHeightCache = () => {
heights.value.clear()
initPositions()
}
const initScroll = () => {
initPositions()
nextTick(() => {
if (scrollerRef.value) {
clientHeight.value = scrollerRef.value.clientHeight
if (props.renderMode === 'bottom') {
scrollToBottom()
}
}
})
}
watch(() => props.data, () => {
const oldScrollTop = scrollerRef.value?.scrollTop || 0
const oldScrollHeight = scrollerRef.value?.scrollHeight || 0
initPositions()
nextTick(() => {
if (scrollerRef.value && props.renderMode === 'bottom') {
const newScrollHeight = scrollerRef.value.scrollHeight
const heightDiff = newScrollHeight - oldScrollHeight
if (heightDiff > 0) {
scrollerRef.value.scrollTop = oldScrollTop + heightDiff
}
}
})
}, { deep: true, immediate: true })
return {
visibleData,
totalHeight,
scrollToBottom,
scrollToTop,
scrollToIndex,
updateData,
getScrollPosition,
setScrollPosition,
updateItemHeight,
clearHeightCache,
handleScroll,
initScroll
}
}