/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { defineComponent, h, useSlots, ref, toRefs, watch, onMounted, onBeforeUnmount } from 'vue'
import { useAnimationFrame } from './useAnimationFrame'
import { useElementSize } from './useElementSize'
/**
* @description: 抽象虚拟滚动组件
* @return {*}
*/
export interface IUseVirtualListProps {
/**
* @description: 传递进来的数据
*/
list: Array<any>
/**
* @description: 数据的唯一 key
*/
key: string
/**
* @description: 每一项的高度
*/
itemHeight: number
/**
* @description: 可视区展示的数量
*/
visualCount: number
/**
* @description: 当前位置
*/
currentIndex?: number
/**
* @description: 容器层的高度
*/
height?: number
}
export const UseVirtualList = defineComponent<IUseVirtualListProps>({
name: 'UseVirtualList',
props: [
'list',
'key',
'itemHeight',
'visualCount',
'currentIndex',
'height',
] as unknown as undefined,
emits: ['toSwitch'],
setup(props, { emit }) {
const { requestAnimateFrame, cancelAnimateFrame } = useAnimationFrame()
const slots = useSlots()
const { list, itemHeight, visualCount } = toRefs(props)
const root = ref<HTMLElement | null>(null)
const scrollHeight = ref(list.value.length * itemHeight.value)
const range: number[] = []
const paddingTop = ref(0)
const pool = ref([] as any[])
const visualHight = ref(0)
let cancelFrame: any
let isScrollBusy = false
watch(list, (cData) => {
console.log('是否死循环')
scrollHeight.value = cData.length * itemHeight.value
pool.value = list.value
.slice(range[0], range[1])
.map((v, i) => ({ ...v, _index: (range[0] || 0) + i }))
})
const handleClick = (item: any, index: number) => {
emit('toSwitch', item, index)
}
const handleScroll = () => {
if (!root.value) return
if (isScrollBusy) return
isScrollBusy = true
// 每次先清除动画
cancelFrame && cancelAnimateFrame(cancelFrame)
cancelFrame = requestAnimateFrame(() => {
isScrollBusy = false
if (!root.value) return
range[0] =
Math.floor(root.value.scrollTop / itemHeight.value) - Math.floor(visualCount.value / 2)
range[0] = Math.max(range[0], 0)
range[1] =
range[0] + Math.floor(root.value.clientHeight / itemHeight.value) + visualCount.value
range[1] = Math.min(range[1], list.value.length)
pool.value = list.value
.slice(range[0], range[1])
.map((v, i) => ({ ...v, _index: (range[0] || 0) + i }))
paddingTop.value = range[0] * itemHeight.value
})
}
onMounted(() => {
if (!root.value) return
visualHight.value = useElementSize(root).height
const contentLines = Math.ceil(visualHight.value / itemHeight.value)
const totalLines = contentLines + visualCount.value
const range = [0, totalLines]
pool.value = list.value
.slice(range[0], range[0] + range[1])
.map((v, i) => ({ ...v, _index: range[0] + i }))
})
onBeforeUnmount(() => {
// 清除动画
cancelFrame && cancelAnimateFrame(cancelFrame)
cancelFrame = null
})
return () =>
h(
'div',
{
ref: root,
class: 'vue3-virtual-list-container',
onScroll: handleScroll,
style: `height: ${props.height ? props.height : visualHight}px`,
},
[
h(
'div',
{
class: 'vue3-virtual-list-scroll',
style: `height: ${scrollHeight.value}px;padding-top: ${paddingTop.value}px`,
},
[
pool.value.map((child) => {
return h(
'li',
{
key: props.key,
style: `height: ${props.itemHeight}px`,
class: { active: child._index === props.currentIndex },
onClick: handleClick.bind({}, child, child._index),
},
[slots.default?.({ item: child, index: child._index })]
)
}),
]
),
]
)
},
})
useElementSize
export interface ElementHeightOrWidth {
height: number
width: number
}
/**
* @description: 获取元素的宽高
* @param {Ref} ele
* @return {*}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export const useElementSize = (ele: any): ElementHeightOrWidth => {
const obj: ElementHeightOrWidth = {
height: 0,
width: 0,
}
if (window.getComputedStyle(ele.value)) {
const height = (window.getComputedStyle(ele.value).height.split('px')[0] || 0) as number
const width = (window.getComputedStyle(ele.value).width.split('px')[0] || 0) as number
obj.height = height
obj.width = width
} else if (ele.value.getBoundingClientRect) {
const height = ele.value.getBoundingClientRect().height
const width = ele.value.getBoundingClientRect().width
obj.height = height
obj.width = width
} else {
obj.height = ele.value.clientHeight
obj.width = ele.value.clientWidth
}
return obj
}