2025-10-23 15:30:21 +08:00
|
|
|
<!-- 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
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
|
2025-10-23 16:01:35 +08:00
|
|
|
const handleScroll = () => {
|
2025-10-23 15:30:21 +08:00
|
|
|
isVisible.value = window.scrollY > props.threshold
|
2025-10-23 16:01:35 +08:00
|
|
|
}
|
2025-10-23 15:30:21 +08:00
|
|
|
|
|
|
|
|
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>
|