pref: optimize virtual scroll (#111)
This commit is contained in:
@@ -31,20 +31,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ResponseScroll
|
<ResponseScroll :items="list" :itemSize="itemSize" class="h-full flex-1">
|
||||||
ref="responseScroll"
|
|
||||||
:items="list"
|
|
||||||
:itemSize="itemSize"
|
|
||||||
:row-key="(item) => item.map(genModelKey).join(',')"
|
|
||||||
class="h-full flex-1"
|
|
||||||
>
|
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 justify-center gap-8 px-8"
|
class="grid grid-cols-1 justify-center gap-8 px-8"
|
||||||
:style="contentStyle"
|
:style="contentStyle"
|
||||||
>
|
>
|
||||||
<ModelCard
|
<ModelCard
|
||||||
v-for="model in item"
|
v-for="model in item.row"
|
||||||
:key="genModelKey(model)"
|
:key="genModelKey(model)"
|
||||||
:model="model"
|
:model="model"
|
||||||
></ModelCard>
|
></ModelCard>
|
||||||
@@ -75,7 +69,7 @@ import { chunk } from 'lodash'
|
|||||||
import { app } from 'scripts/comfyAPI'
|
import { app } from 'scripts/comfyAPI'
|
||||||
import { Model } from 'types/typings'
|
import { Model } from 'types/typings'
|
||||||
import { genModelKey } from 'utils/model'
|
import { genModelKey } from 'utils/model'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { isMobile, cardWidth, gutter, aspect } = useConfig()
|
const { isMobile, cardWidth, gutter, aspect } = useConfig()
|
||||||
@@ -89,8 +83,6 @@ const { $2xl: $toolbar_2xl } = useContainerQueries(toolbarContainer)
|
|||||||
const contentContainer = ref<HTMLElement | null>(null)
|
const contentContainer = ref<HTMLElement | null>(null)
|
||||||
const { $lg: $content_lg } = useContainerQueries(contentContainer)
|
const { $lg: $content_lg } = useContainerQueries(contentContainer)
|
||||||
|
|
||||||
const responseScroll = ref()
|
|
||||||
|
|
||||||
const searchContent = ref<string>()
|
const searchContent = ref<string>()
|
||||||
|
|
||||||
const currentType = ref('all')
|
const currentType = ref('all')
|
||||||
@@ -133,10 +125,6 @@ const sortOrderOptions = ref(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
watch([searchContent, currentType], () => {
|
|
||||||
responseScroll.value.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
const itemSize = computed(() => {
|
const itemSize = computed(() => {
|
||||||
let itemWidth = cardWidth
|
let itemWidth = cardWidth
|
||||||
let itemGutter = gutter
|
let itemGutter = gutter
|
||||||
@@ -193,7 +181,9 @@ const list = computed(() => {
|
|||||||
|
|
||||||
const sortedList = filterList.sort(sortStrategy)
|
const sortedList = filterList.sort(sortStrategy)
|
||||||
|
|
||||||
return chunk(sortedList, cols.value)
|
return chunk(sortedList, cols.value).map((row) => {
|
||||||
|
return { key: row.map(genModelKey).join(','), row }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const contentStyle = computed(() => ({
|
const contentStyle = computed(() => ({
|
||||||
|
|||||||
@@ -1,57 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div data-scroll-area class="group/scroll relative overflow-hidden">
|
<div class="group/scroll relative overflow-hidden">
|
||||||
<div
|
<div ref="viewport" class="h-full w-full overflow-auto scrollbar-none">
|
||||||
ref="viewport"
|
<div ref="content">
|
||||||
data-scroll-viewport
|
|
||||||
class="h-full w-full overflow-auto scrollbar-none"
|
|
||||||
:style="{ contain: items ? 'strict' : undefined }"
|
|
||||||
>
|
|
||||||
<div data-scroll-content class="relative min-w-full">
|
|
||||||
<slot name="default">
|
<slot name="default">
|
||||||
<div
|
<slot v-if="renderedItems.length === 0" name="empty">
|
||||||
v-for="(item, index) in loadedItems"
|
|
||||||
:key="genRowKey(item, index)"
|
|
||||||
:style="{ height: `${itemSize}px` }"
|
|
||||||
>
|
|
||||||
<slot name="item" :item="item"></slot>
|
|
||||||
</div>
|
|
||||||
<slot v-if="loadedItems.length === 0" name="empty">
|
|
||||||
<div class="absolute w-full py-20 text-center">No Data</div>
|
<div class="absolute w-full py-20 text-center">No Data</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
|
<div :style="{ height: `${headHeight}px` }"></div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="item in renderedItems"
|
||||||
|
:key="item.key"
|
||||||
|
:style="{ height: `${itemSize}px` }"
|
||||||
|
data-virtual-item
|
||||||
|
>
|
||||||
|
<slot name="item" :item="item"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :style="{ height: `${tailHeight}px` }"></div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
data-scroll-space
|
|
||||||
class="pointer-events-none absolute left-0 top-0 h-px w-px"
|
|
||||||
:style="spaceStyle"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="absolute right-0 top-0 h-full w-2">
|
||||||
v-for="scroll in scrollbars"
|
|
||||||
:key="scroll.direction"
|
|
||||||
v-show="scroll.visible"
|
|
||||||
v-bind="{ [`data-scroll-bar-${scroll.direction}`]: '' }"
|
|
||||||
:class="[
|
|
||||||
'pointer-events-none absolute z-auto h-full w-full rounded-full',
|
|
||||||
'data-[scroll-bar-horizontal]:bottom-0 data-[scroll-bar-horizontal]:left-0 data-[scroll-bar-horizontal]:h-2',
|
|
||||||
'data-[scroll-bar-vertical]:right-0 data-[scroll-bar-vertical]:top-0 data-[scroll-bar-vertical]:w-2',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-bind="{ ['data-scroll-thumb']: scroll.direction }"
|
|
||||||
:class="[
|
:class="[
|
||||||
'pointer-events-auto absolute h-full w-full rounded-full',
|
'absolute w-full cursor-pointer rounded-full bg-gray-500',
|
||||||
'cursor-pointer bg-black dark:bg-white',
|
'opacity-0 transition-opacity duration-300 group-hover/scroll:opacity-30',
|
||||||
'opacity-0 transition-opacity duration-300 group-hover/scroll:opacity-10',
|
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{ height: `${thumbSize}px`, top: `${thumbOffset}px` }"
|
||||||
[scrollbarAttrs[scroll.direction].size]: `${scroll.size}px`,
|
|
||||||
[scrollbarAttrs[scroll.direction].offset]: `${scroll.offset}px`,
|
|
||||||
opacity: isDragging ? 0.1 : '',
|
|
||||||
}"
|
|
||||||
@mousedown="startDragThumb"
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,258 +38,75 @@
|
|||||||
<script setup lang="ts" generic="T">
|
<script setup lang="ts" generic="T">
|
||||||
import { useContainerResize } from 'hooks/resize'
|
import { useContainerResize } from 'hooks/resize'
|
||||||
import { useContainerScroll } from 'hooks/scroll'
|
import { useContainerScroll } from 'hooks/scroll'
|
||||||
import { clamp, throttle } from 'lodash'
|
import { clamp } from 'lodash'
|
||||||
import { nextTick, onUnmounted, ref, watch } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
interface ScrollAreaProps {
|
interface ScrollAreaProps {
|
||||||
items?: T[][]
|
items?: (T & { key: string })[]
|
||||||
itemSize?: number
|
itemSize?: number
|
||||||
scrollbar?: boolean
|
|
||||||
rowKey?: string | ((item: T[]) => string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ScrollAreaProps>(), {
|
const props = defineProps<ScrollAreaProps>()
|
||||||
scrollbar: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
type ScrollbarDirection = 'horizontal' | 'vertical'
|
|
||||||
|
|
||||||
interface Scrollbar {
|
|
||||||
direction: ScrollbarDirection
|
|
||||||
visible: boolean
|
|
||||||
size: number
|
|
||||||
offset: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScrollbarAttribute {
|
|
||||||
clientSize: string
|
|
||||||
scrollOffset: string
|
|
||||||
pagePosition: string
|
|
||||||
offset: string
|
|
||||||
size: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollbarAttrs: Record<ScrollbarDirection, ScrollbarAttribute> = {
|
|
||||||
horizontal: {
|
|
||||||
clientSize: 'clientWidth',
|
|
||||||
scrollOffset: 'scrollLeft',
|
|
||||||
pagePosition: 'pageX',
|
|
||||||
offset: 'left',
|
|
||||||
size: 'width',
|
|
||||||
},
|
|
||||||
vertical: {
|
|
||||||
clientSize: 'clientHeight',
|
|
||||||
scrollOffset: 'scrollTop',
|
|
||||||
pagePosition: 'pageY',
|
|
||||||
offset: 'top',
|
|
||||||
size: 'height',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollbars = ref<Record<ScrollbarDirection, Scrollbar>>({
|
|
||||||
horizontal: {
|
|
||||||
direction: 'horizontal',
|
|
||||||
visible: props.scrollbar,
|
|
||||||
size: 0,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
vertical: {
|
|
||||||
direction: 'vertical',
|
|
||||||
visible: props.scrollbar,
|
|
||||||
size: 0,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const isDragging = ref(false)
|
|
||||||
|
|
||||||
const spaceStyle = ref({})
|
|
||||||
const loadedItems = ref<T[][]>([])
|
|
||||||
|
|
||||||
const genRowKey = (item: any | any[], index: number) => {
|
|
||||||
if (typeof props.rowKey === 'function') {
|
|
||||||
return props.rowKey(item)
|
|
||||||
}
|
|
||||||
return item[props.rowKey ?? 'key'] ?? index
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSpacerSize = () => {
|
|
||||||
const items = props.items
|
|
||||||
if (items) {
|
|
||||||
const itemSize = props.itemSize ?? 0
|
|
||||||
spaceStyle.value = { height: `${itemSize * items.length}px` }
|
|
||||||
} else {
|
|
||||||
spaceStyle.value = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getContainerContent = (raw?: boolean): HTMLElement => {
|
|
||||||
const container = viewport.value as HTMLElement
|
|
||||||
|
|
||||||
if (props.items && !raw) {
|
|
||||||
return container.querySelector('[data-scroll-space]')!
|
|
||||||
}
|
|
||||||
return container.querySelector('[data-scroll-content]')!
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
const container = viewport.value as HTMLElement
|
|
||||||
container.scrollTop = 0
|
|
||||||
getContainerContent().style.transform = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateLoadItems = () => {
|
|
||||||
let visibleItems: any[] = []
|
|
||||||
|
|
||||||
if (props.items) {
|
|
||||||
const container = viewport.value as HTMLElement
|
|
||||||
const content = getContainerContent(true)
|
|
||||||
|
|
||||||
const resolveVisibleItems = (items: any[], attr: ScrollbarAttribute) => {
|
|
||||||
const containerSize = container[attr.clientSize]
|
|
||||||
const itemSize = props.itemSize!
|
|
||||||
const viewCount = Math.ceil(containerSize / itemSize)
|
|
||||||
|
|
||||||
let start = Math.floor(container[attr.scrollOffset] / itemSize)
|
|
||||||
const offset = start * itemSize
|
|
||||||
|
|
||||||
let end = start + viewCount
|
|
||||||
end = Math.min(end + viewCount, items.length)
|
|
||||||
|
|
||||||
content.style.transform = `translateY(${offset}px)`
|
|
||||||
return items.slice(start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleItems = resolveVisibleItems(props.items, scrollbarAttrs.vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedItems.value = visibleItems
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateScrollThumbSize = () => {
|
|
||||||
const container = viewport.value as HTMLElement
|
|
||||||
const content = getContainerContent()
|
|
||||||
|
|
||||||
const resolveScrollbarSize = (item: Scrollbar, attr: ScrollbarAttribute) => {
|
|
||||||
const containerSize: number = container[attr.clientSize]
|
|
||||||
const contentSize: number = content[attr.clientSize]
|
|
||||||
item.visible = props.scrollbar && contentSize > containerSize
|
|
||||||
item.size = Math.max(Math.pow(containerSize, 2) / contentSize, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
resolveScrollbarSize(scrollbars.value.horizontal, scrollbarAttrs.horizontal)
|
|
||||||
resolveScrollbarSize(scrollbars.value.vertical, scrollbarAttrs.vertical)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = ref<HTMLElement | null>(null)
|
const viewport = ref<HTMLElement | null>(null)
|
||||||
|
const content = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const { width, height } = useContainerResize(viewport)
|
const { height: viewportHeight } = useContainerResize(viewport)
|
||||||
|
const { height: contentHeight } = useContainerResize(content)
|
||||||
|
const { y: scrollY } = useContainerScroll(viewport)
|
||||||
|
|
||||||
watch([width, height], () => {
|
const itemSize = computed(() => props.itemSize || 0)
|
||||||
if (isDragging.value) return
|
|
||||||
|
|
||||||
calculateScrollThumbSize()
|
const viewRows = computed(() =>
|
||||||
|
Math.ceil(viewportHeight.value / itemSize.value),
|
||||||
|
)
|
||||||
|
const offsetRows = computed(() => Math.floor(scrollY.value / itemSize.value))
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
return props.items ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
useContainerScroll(viewport, {
|
const state = computed(() => {
|
||||||
onScroll: (event) => {
|
const bufferRows = viewRows.value
|
||||||
if (isDragging.value) return
|
|
||||||
|
|
||||||
const container = event.target as HTMLDivElement
|
const fromRow = offsetRows.value - bufferRows
|
||||||
const content = getContainerContent()
|
const toRow = offsetRows.value + bufferRows + viewRows.value
|
||||||
|
|
||||||
const resolveOffset = (item: Scrollbar, attr: ScrollbarAttribute) => {
|
const itemCount = items.value.length
|
||||||
const containerSize = container[attr.clientSize]
|
|
||||||
const contentSize = content[attr.clientSize]
|
|
||||||
const scrollOffset = container[attr.scrollOffset]
|
|
||||||
|
|
||||||
item.offset =
|
return {
|
||||||
(scrollOffset / (contentSize - containerSize)) *
|
start: clamp(fromRow, 0, itemCount),
|
||||||
(containerSize - item.size)
|
end: clamp(toRow, fromRow, itemCount),
|
||||||
}
|
|
||||||
|
|
||||||
resolveOffset(scrollbars.value.horizontal, scrollbarAttrs.horizontal)
|
|
||||||
resolveOffset(scrollbars.value.vertical, scrollbarAttrs.vertical)
|
|
||||||
|
|
||||||
calculateLoadItems()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const draggingDirection = ref<ScrollbarDirection>()
|
|
||||||
const prevDraggingEvent = ref<MouseEvent>()
|
|
||||||
|
|
||||||
const moveThumb = throttle((event: MouseEvent) => {
|
|
||||||
if (isDragging.value) {
|
|
||||||
const container = viewport.value!
|
|
||||||
const content = getContainerContent()
|
|
||||||
|
|
||||||
const resolveOffset = (item: Scrollbar, attr: ScrollbarAttribute) => {
|
|
||||||
const containerSize = container[attr.clientSize]
|
|
||||||
const contentSize = content[attr.clientSize]
|
|
||||||
|
|
||||||
// Resolve thumb position
|
|
||||||
const prevPagePos = prevDraggingEvent.value![attr.pagePosition]
|
|
||||||
const currPagePos = event[attr.pagePosition]
|
|
||||||
const offset = currPagePos - prevPagePos
|
|
||||||
item.offset = clamp(item.offset + offset, 0, containerSize - item.size)
|
|
||||||
|
|
||||||
// Resolve scroll position
|
|
||||||
const scrollOffset = containerSize - item.size
|
|
||||||
const offsetSize = contentSize - containerSize
|
|
||||||
|
|
||||||
container[attr.scrollOffset] = (item.offset / scrollOffset) * offsetSize
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollDirection = draggingDirection.value!
|
|
||||||
|
|
||||||
resolveOffset(
|
|
||||||
scrollbars.value[scrollDirection],
|
|
||||||
scrollbarAttrs[scrollDirection],
|
|
||||||
)
|
|
||||||
prevDraggingEvent.value = event
|
|
||||||
|
|
||||||
calculateLoadItems()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const stopMoveThumb = () => {
|
const renderedItems = computed(() => {
|
||||||
isDragging.value = false
|
const { start, end } = state.value
|
||||||
draggingDirection.value = undefined
|
|
||||||
prevDraggingEvent.value = undefined
|
|
||||||
document.removeEventListener('mousemove', moveThumb)
|
|
||||||
document.removeEventListener('mouseup', stopMoveThumb)
|
|
||||||
document.body.style.userSelect = ''
|
|
||||||
document.body.style.cursor = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDragThumb = (event: MouseEvent) => {
|
return props.items?.slice(start, end) ?? []
|
||||||
isDragging.value = true
|
|
||||||
const target = event.target as HTMLElement
|
|
||||||
draggingDirection.value = <any>target.getAttribute('data-scroll-thumb')
|
|
||||||
prevDraggingEvent.value = event
|
|
||||||
document.addEventListener('mousemove', moveThumb)
|
|
||||||
document.addEventListener('mouseup', stopMoveThumb)
|
|
||||||
document.body.style.userSelect = 'none'
|
|
||||||
document.body.style.cursor = 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.items,
|
|
||||||
() => {
|
|
||||||
setSpacerSize()
|
|
||||||
calculateScrollThumbSize()
|
|
||||||
calculateLoadItems()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopMoveThumb()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
const headHeight = computed(() => {
|
||||||
viewport,
|
return state.value.start * itemSize.value
|
||||||
init,
|
})
|
||||||
|
|
||||||
|
const tailHeight = computed(() => {
|
||||||
|
return (items.value.length - state.value.end) * itemSize.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const thumbSize = computed(() => {
|
||||||
|
if (viewportHeight.value >= contentHeight.value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbHeight = Math.pow(viewportHeight.value, 2) / contentHeight.value
|
||||||
|
return Math.max(thumbHeight, 16)
|
||||||
|
})
|
||||||
|
|
||||||
|
const thumbOffset = computed(() => {
|
||||||
|
return (
|
||||||
|
(scrollY.value / (contentHeight.value - viewportHeight.value)) *
|
||||||
|
(viewportHeight.value - thumbSize.value)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
import { type Ref, onUnmounted, ref, toRef, watch } from 'vue'
|
import { type Ref, onUnmounted, ref, toRef, watch } from 'vue'
|
||||||
|
|
||||||
export const defineResizeCallback = (
|
|
||||||
callback: ResizeObserverCallback,
|
|
||||||
wait?: number,
|
|
||||||
) => {
|
|
||||||
return throttle(callback, wait ?? 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useContainerResize = (
|
export const useContainerResize = (
|
||||||
el: HTMLElement | null | Ref<HTMLElement | null>,
|
el: HTMLElement | null | Ref<HTMLElement | null>,
|
||||||
) => {
|
) => {
|
||||||
@@ -20,11 +13,11 @@ export const useContainerResize = (
|
|||||||
toRef(el),
|
toRef(el),
|
||||||
(el) => {
|
(el) => {
|
||||||
if (el) {
|
if (el) {
|
||||||
const onResize = defineResizeCallback((entries) => {
|
const onResize = throttle((entries: ResizeObserverEntry[]) => {
|
||||||
const entry = entries[0]
|
const entry = entries[0]
|
||||||
width.value = entry.contentRect.width
|
width.value = entry.contentRect.width
|
||||||
height.value = entry.contentRect.height
|
height.value = entry.contentRect.height
|
||||||
})
|
}, 64)
|
||||||
|
|
||||||
observer.value = new ResizeObserver(onResize)
|
observer.value = new ResizeObserver(onResize)
|
||||||
observer.value.observe(el)
|
observer.value.observe(el)
|
||||||
|
|||||||
@@ -22,29 +22,29 @@ export const useContainerScroll = (
|
|||||||
scrollLeft.value = container.value.scrollLeft
|
scrollLeft.value = container.value.scrollLeft
|
||||||
scrollTop.value = container.value.scrollTop
|
scrollTop.value = container.value.scrollTop
|
||||||
}
|
}
|
||||||
}, options?.throttle || 100)
|
}, options?.throttle ?? 64)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
container,
|
container,
|
||||||
(el) => {
|
(el) => {
|
||||||
if (el) {
|
if (el) {
|
||||||
el.addEventListener('scroll', onScroll)
|
el.addEventListener('scroll', onScroll, { passive: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const x = computed({
|
const x = computed({
|
||||||
get: () => scrollLeft,
|
get: () => scrollLeft.value,
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
container.value?.scrollTo({ left: val.value })
|
container.value?.scrollTo({ left: val })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const y = computed({
|
const y = computed({
|
||||||
get: () => scrollTop,
|
get: () => scrollTop.value,
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
container.value?.scrollTo({ top: val.value })
|
container.value?.scrollTo({ top: val })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user