mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
411 lines
11 KiB
Vue
411 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { useEventListener, useResizeObserver, useScroll, useTimeoutFn } from '@vueuse/core'
|
|
import { clamp } from 'es-toolkit'
|
|
import { computed, nextTick, onMounted, shallowRef, useTemplateRef } from 'vue'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
viewClass?: string
|
|
maxHeight?: string
|
|
horizontal?: boolean
|
|
vertical?: boolean
|
|
}>(),
|
|
{
|
|
viewClass: '',
|
|
maxHeight: undefined,
|
|
horizontal: false,
|
|
vertical: true,
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
scroll: [payload: { scrollTop: number; scrollLeft: number }]
|
|
}>()
|
|
|
|
const wrapRef = useTemplateRef<HTMLDivElement>('wrap')
|
|
const viewRef = useTemplateRef<HTMLElement>('view')
|
|
const thumbHeight = shallowRef(0)
|
|
const thumbTop = shallowRef(0)
|
|
const horizontalThumbWidth = shallowRef(0)
|
|
const horizontalThumbLeft = shallowRef(0)
|
|
const visible = shallowRef(false)
|
|
const verticalTrackHovering = shallowRef(false)
|
|
const horizontalTrackHovering = shallowRef(false)
|
|
const dragging = shallowRef(false)
|
|
const horizontalDragging = shallowRef(false)
|
|
|
|
let dragStartY = 0
|
|
let dragStartScrollTop = 0
|
|
let horizontalDragStartX = 0
|
|
let horizontalDragStartScrollLeft = 0
|
|
const { start: startHideTimer, stop: stopHideTimer } = useTimeoutFn(hideScrollbar, 1600, {
|
|
immediate: false,
|
|
})
|
|
|
|
const canScrollY = computed(() => thumbHeight.value > 0)
|
|
const canScrollX = computed(() => horizontalThumbWidth.value > 0)
|
|
const verticalScrollbarVisible = computed(
|
|
() => dragging.value || verticalTrackHovering.value || visible.value,
|
|
)
|
|
const horizontalScrollbarVisible = computed(
|
|
() => horizontalDragging.value || horizontalTrackHovering.value || visible.value,
|
|
)
|
|
const thumbStyle = computed(() => ({
|
|
height: `${thumbHeight.value}px`,
|
|
transform: `translateY(${thumbTop.value}px)`,
|
|
}))
|
|
const horizontalThumbStyle = computed(() => ({
|
|
width: `${horizontalThumbWidth.value}px`,
|
|
transform: `translateX(${horizontalThumbLeft.value}px)`,
|
|
}))
|
|
const rootClasses = computed(() => [
|
|
'relative min-h-0 overflow-hidden',
|
|
props.maxHeight ? undefined : 'h-full',
|
|
])
|
|
const wrapClasses = computed(() => [
|
|
'min-h-0 overflow-auto max-h-[inherit] [-ms-overflow-style:none] scrollbar-none [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar]:w-0 [&::-webkit-scrollbar]:bg-transparent',
|
|
props.maxHeight ? undefined : 'h-full',
|
|
])
|
|
const verticalTrackClass = computed(() =>
|
|
props.horizontal && canScrollX.value ? 'bottom-3' : 'bottom-1',
|
|
)
|
|
const horizontalTrackClass = computed(() =>
|
|
props.vertical && canScrollY.value ? 'right-3' : 'right-1',
|
|
)
|
|
|
|
function trackHeight(wrap: HTMLElement) {
|
|
return clamp(wrap.clientHeight - 8, 0, Number.POSITIVE_INFINITY)
|
|
}
|
|
|
|
function maxScrollTop(wrap: HTMLElement) {
|
|
return clamp(wrap.scrollHeight - wrap.clientHeight, 0, Number.POSITIVE_INFINITY)
|
|
}
|
|
|
|
function trackWidth(wrap: HTMLElement) {
|
|
return clamp(wrap.clientWidth - 8, 0, Number.POSITIVE_INFINITY)
|
|
}
|
|
|
|
function maxScrollLeft(wrap: HTMLElement) {
|
|
return clamp(wrap.scrollWidth - wrap.clientWidth, 0, Number.POSITIVE_INFINITY)
|
|
}
|
|
|
|
function maxThumbTop(wrap: HTMLElement) {
|
|
return clamp(trackHeight(wrap) - thumbHeight.value, 0, Number.POSITIVE_INFINITY)
|
|
}
|
|
|
|
function maxHorizontalThumbLeft(wrap: HTMLElement) {
|
|
return clamp(trackWidth(wrap) - horizontalThumbWidth.value, 0, Number.POSITIVE_INFINITY)
|
|
}
|
|
|
|
function showScrollbar() {
|
|
visible.value = true
|
|
}
|
|
|
|
function clearHideTimer() {
|
|
stopHideTimer()
|
|
}
|
|
|
|
function scheduleHideScrollbar() {
|
|
clearHideTimer()
|
|
startHideTimer()
|
|
}
|
|
|
|
function hideScrollbar() {
|
|
clearHideTimer()
|
|
visible.value = false
|
|
}
|
|
|
|
function activateScrollbar() {
|
|
showScrollbar()
|
|
scheduleHideScrollbar()
|
|
}
|
|
|
|
type ScrollbarAxis = 'vertical' | 'horizontal'
|
|
|
|
function setTrackHovering(axis: ScrollbarAxis, hovering: boolean) {
|
|
if (axis === 'vertical') {
|
|
verticalTrackHovering.value = hovering
|
|
return
|
|
}
|
|
|
|
horizontalTrackHovering.value = hovering
|
|
}
|
|
|
|
function handleTrackMouseEnter(axis: ScrollbarAxis) {
|
|
setTrackHovering(axis, true)
|
|
showScrollbar()
|
|
scheduleHideScrollbar()
|
|
}
|
|
|
|
function handleTrackMouseLeave(axis: ScrollbarAxis) {
|
|
setTrackHovering(axis, false)
|
|
scheduleHideScrollbar()
|
|
}
|
|
|
|
function update() {
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
updateVerticalScrollbar(wrap)
|
|
updateHorizontalScrollbar(wrap)
|
|
}
|
|
|
|
function updateVerticalScrollbar(wrap: HTMLElement) {
|
|
if (!props.vertical) {
|
|
thumbHeight.value = 0
|
|
thumbTop.value = 0
|
|
return
|
|
}
|
|
|
|
const scrollRange = maxScrollTop(wrap)
|
|
const availableTrackHeight = trackHeight(wrap)
|
|
if (scrollRange <= 0 || availableTrackHeight <= 0) {
|
|
thumbHeight.value = 0
|
|
thumbTop.value = 0
|
|
return
|
|
}
|
|
const ratio = wrap.clientHeight / wrap.scrollHeight
|
|
thumbHeight.value = clamp(availableTrackHeight * ratio, 32, availableTrackHeight)
|
|
thumbTop.value = (wrap.scrollTop / scrollRange) * maxThumbTop(wrap)
|
|
}
|
|
|
|
function updateHorizontalScrollbar(wrap: HTMLElement) {
|
|
if (!props.horizontal) {
|
|
horizontalThumbWidth.value = 0
|
|
horizontalThumbLeft.value = 0
|
|
return
|
|
}
|
|
|
|
const scrollRange = maxScrollLeft(wrap)
|
|
const availableTrackWidth = trackWidth(wrap)
|
|
if (scrollRange <= 0 || availableTrackWidth <= 0) {
|
|
horizontalThumbWidth.value = 0
|
|
horizontalThumbLeft.value = 0
|
|
return
|
|
}
|
|
const ratio = wrap.clientWidth / wrap.scrollWidth
|
|
horizontalThumbWidth.value = clamp(availableTrackWidth * ratio, 32, availableTrackWidth)
|
|
horizontalThumbLeft.value = (wrap.scrollLeft / scrollRange) * maxHorizontalThumbLeft(wrap)
|
|
}
|
|
|
|
async function scrollToTop() {
|
|
const wrap = wrapRef.value
|
|
if (wrap) {
|
|
wrap.scrollTop = 0
|
|
wrap.scrollLeft = 0
|
|
}
|
|
dragging.value = false
|
|
horizontalDragging.value = false
|
|
visible.value = false
|
|
clearHideTimer()
|
|
await nextTick()
|
|
update()
|
|
}
|
|
|
|
function handleScroll() {
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
update()
|
|
activateScrollbar()
|
|
emit('scroll', {
|
|
scrollTop: wrap.scrollTop,
|
|
scrollLeft: wrap.scrollLeft,
|
|
})
|
|
}
|
|
|
|
function handleTrackPointerDown(event: PointerEvent) {
|
|
if (event.target !== event.currentTarget) {
|
|
return
|
|
}
|
|
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
|
activateScrollbar()
|
|
const nextThumbTop = event.clientY - rect.top - thumbHeight.value / 2
|
|
const scrollRange = maxScrollTop(wrap)
|
|
const thumbRange = maxThumbTop(wrap)
|
|
wrap.scrollTop =
|
|
thumbRange > 0 ? (clamp(nextThumbTop, 0, thumbRange) / thumbRange) * scrollRange : 0
|
|
}
|
|
|
|
function handleHorizontalTrackPointerDown(event: PointerEvent) {
|
|
if (event.target !== event.currentTarget) {
|
|
return
|
|
}
|
|
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
|
activateScrollbar()
|
|
const nextThumbLeft = event.clientX - rect.left - horizontalThumbWidth.value / 2
|
|
const scrollRange = maxScrollLeft(wrap)
|
|
const thumbRange = maxHorizontalThumbLeft(wrap)
|
|
wrap.scrollLeft =
|
|
thumbRange > 0 ? (clamp(nextThumbLeft, 0, thumbRange) / thumbRange) * scrollRange : 0
|
|
}
|
|
|
|
function handleThumbPointerDown(event: PointerEvent) {
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
dragging.value = true
|
|
visible.value = true
|
|
clearHideTimer()
|
|
dragStartY = event.clientY
|
|
dragStartScrollTop = wrap.scrollTop
|
|
}
|
|
|
|
function handleHorizontalThumbPointerDown(event: PointerEvent) {
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
horizontalDragging.value = true
|
|
visible.value = true
|
|
clearHideTimer()
|
|
horizontalDragStartX = event.clientX
|
|
horizontalDragStartScrollLeft = wrap.scrollLeft
|
|
}
|
|
|
|
function handleThumbPointerMove(event: PointerEvent) {
|
|
if (!dragging.value) {
|
|
return
|
|
}
|
|
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
const thumbRange = maxThumbTop(wrap)
|
|
if (thumbRange <= 0) {
|
|
return
|
|
}
|
|
|
|
const scrollRange = maxScrollTop(wrap)
|
|
wrap.scrollTop = dragStartScrollTop + ((event.clientY - dragStartY) / thumbRange) * scrollRange
|
|
}
|
|
|
|
function handleHorizontalThumbPointerMove(event: PointerEvent) {
|
|
if (!horizontalDragging.value) {
|
|
return
|
|
}
|
|
|
|
const wrap = wrapRef.value
|
|
if (!wrap) {
|
|
return
|
|
}
|
|
|
|
const thumbRange = maxHorizontalThumbLeft(wrap)
|
|
if (thumbRange <= 0) {
|
|
return
|
|
}
|
|
|
|
const scrollRange = maxScrollLeft(wrap)
|
|
wrap.scrollLeft =
|
|
horizontalDragStartScrollLeft +
|
|
((event.clientX - horizontalDragStartX) / thumbRange) * scrollRange
|
|
}
|
|
|
|
function handleThumbPointerUp() {
|
|
if (!dragging.value) {
|
|
return
|
|
}
|
|
|
|
dragging.value = false
|
|
activateScrollbar()
|
|
}
|
|
|
|
function handleHorizontalThumbPointerUp() {
|
|
if (!horizontalDragging.value) {
|
|
return
|
|
}
|
|
|
|
horizontalDragging.value = false
|
|
activateScrollbar()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await nextTick()
|
|
update()
|
|
})
|
|
|
|
useResizeObserver([wrapRef, viewRef], update)
|
|
useScroll(wrapRef, { onScroll: handleScroll })
|
|
useEventListener(document, 'pointermove', handleThumbPointerMove)
|
|
useEventListener(document, 'pointerup', handleThumbPointerUp)
|
|
useEventListener(document, 'pointermove', handleHorizontalThumbPointerMove)
|
|
useEventListener(document, 'pointerup', handleHorizontalThumbPointerUp)
|
|
|
|
defineExpose({
|
|
update,
|
|
scrollToTop,
|
|
wrapRef,
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
:class="rootClasses"
|
|
:style="{ maxHeight }"
|
|
@mouseenter="activateScrollbar"
|
|
@mousemove="activateScrollbar"
|
|
@mouseleave="hideScrollbar"
|
|
>
|
|
<div ref="wrap" :class="wrapClasses">
|
|
<div ref="view" :class="viewClass">
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-show="canScrollY"
|
|
class="absolute top-1 right-1 z-40 w-1.5 rounded-full transition-opacity duration-200"
|
|
:class="[verticalTrackClass, verticalScrollbarVisible ? 'opacity-100' : 'opacity-0']"
|
|
@mouseenter="handleTrackMouseEnter('vertical')"
|
|
@mouseleave="handleTrackMouseLeave('vertical')"
|
|
@pointerdown="handleTrackPointerDown"
|
|
>
|
|
<div
|
|
class="w-full rounded-full bg-(--cp-scrollbar-thumb) transition-colors duration-200 hover:bg-(--cp-scrollbar-thumb-hover)"
|
|
:class="dragging ? 'bg-(--cp-scrollbar-thumb-hover)' : ''"
|
|
:style="thumbStyle"
|
|
@pointerdown="handleThumbPointerDown"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-show="canScrollX"
|
|
class="absolute bottom-1 left-1 z-40 h-1.5 rounded-full transition-opacity duration-200"
|
|
:class="[horizontalTrackClass, horizontalScrollbarVisible ? 'opacity-100' : 'opacity-0']"
|
|
@mouseenter="handleTrackMouseEnter('horizontal')"
|
|
@mouseleave="handleTrackMouseLeave('horizontal')"
|
|
@pointerdown="handleHorizontalTrackPointerDown"
|
|
>
|
|
<div
|
|
class="h-full rounded-full bg-(--cp-scrollbar-thumb) transition-colors duration-200 hover:bg-(--cp-scrollbar-thumb-hover)"
|
|
:class="horizontalDragging ? 'bg-(--cp-scrollbar-thumb-hover)' : ''"
|
|
:style="horizontalThumbStyle"
|
|
@pointerdown="handleHorizontalThumbPointerDown"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|