beehive/app/components/ScrollToTop.vue
2025-10-23 16:14:16 +08:00

106 lines
2.2 KiB
Vue

<!-- components/ScrollToTop.vue -->
<template>
<UButton
v-show="isVisible"
:aria-label="ariaLabel"
:class="buttonClasses"
:icon="icon"
:variant="variant"
@click="handleClick"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>
<!-- 插槽支持自定义内容 -->
<slot>
<span v-if="showText && isHovered" class="ml-2 text-xs font-medium">
{{ text }}
</span>
</slot>
</UButton>
</template>
<script lang="ts" setup>
interface Props {
threshold?: number
position?: 'br' | 'bl' | 'tr' | 'tl' // bottom-right, bottom-left, etc.
size?: 'sm' | 'md' | 'lg'
color?: 'primary' | 'green' | 'blue' | 'gray'
variant?: 'solid' | 'outline' | 'soft'
icon?: string
showText?: boolean
text?: string
ariaLabel?: string
smoothScroll?: boolean
}
const props = withDefaults(defineProps<Props>(), {
threshold: 300,
position: 'br',
size: 'lg',
color: 'green',
variant: 'solid',
icon: 'i-heroicons-arrow-up',
showText: false,
text: '回到顶部',
ariaLabel: '回到页面顶部',
smoothScroll: true
})
const isVisible = ref(false)
const isHovered = ref(false)
// 计算位置类名
const positionClasses = computed(() => {
const map = {
br: 'bottom-6 right-6',
bl: 'bottom-6 left-6',
tr: 'top-6 right-6',
tl: 'top-6 left-6'
}
return map[props.position] || map.br
})
// 计算尺寸类名
const buttonClasses = computed(() => [
'fixed z-50 rounded-full transition-all duration-300 ease-in-out',
positionClasses.value,
{
'opacity-100': isVisible.value,
'opacity-0': !isVisible.value,
'hover:scale-110': isVisible.value,
'px-4': props.showText && isHovered.value
}
])
const handleScroll = () => {
isVisible.value = window.scrollY > props.threshold
}
const handleClick = () => {
if (props.smoothScroll) {
window.scrollTo({top: 0, behavior: 'smooth'})
} else {
window.scrollTo(0, 0)
}
// 触发自定义事件
emit('click')
}
const emit = defineEmits<{
click: []
}>()
onMounted(() => {
window.addEventListener('scroll', handleScroll, {passive: true})
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>