Skip to content

回到顶部按钮

添加一个平滑滚动的回到顶部按钮,提升长页面的用户体验。

组件实现

vue
<!-- .vitepress/theme/components/BackToTop.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const visible = ref(false)
const scrollY = ref(0)

const handleScroll = () => {
  scrollY.value = window.scrollY
  visible.value = window.scrollY > 300
}

const scrollToTop = () => {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  })
}

onMounted(() => {
  window.addEventListener('scroll', handleScroll)
  handleScroll()
})

onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll)
})
</script>

<template>
  <Transition name="fade">
    <button
      v-show="visible"
      class="back-to-top"
      @click="scrollToTop"
      aria-label="返回顶部"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <polyline points="18 15 12 9 6 15"></polyline>
      </svg>
    </button>
  </Transition>
</template>

<style scoped>
.back-to-top {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--vp-c-bg);
  border: 1px solid var(--vp-c-divider);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
  z-index: 99;
}

.back-to-top:hover {
  background: var(--vp-c-brand-1);
  color: white;
  border-color: var(--vp-c-brand-1);
  transform: translateY(-4px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}

.back-to-top:active {
  transform: translateY(-2px);
}

.fade-enter-active,
.fade-leave-active {
  transition: all 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: translateY(20px);
}

/* 深色模式适配 */
.dark .back-to-top {
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
</style>

注册组件

ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import BackToTop from './components/BackToTop.vue'
import { h } from 'vue'

export default {
  extends: DefaultTheme,
  Layout: () => {
    return h(DefaultTheme.Layout, null, {
      'layout-bottom': () => h(BackToTop)
    })
  }
}

高级:带进度指示

vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'

const visible = ref(false)
const scrollY = ref(0)
const docHeight = ref(0)

const progress = computed(() => {
  if (docHeight.value === 0) return 0
  return Math.min(scrollY.value / (docHeight.value - window.innerHeight), 1)
})

const handleScroll = () => {
  scrollY.value = window.scrollY
  docHeight.value = document.documentElement.scrollHeight
  visible.value = window.scrollY > 300
}

const scrollToTop = () => {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  })
}

onMounted(() => {
  window.addEventListener('scroll', handleScroll)
  handleScroll()
})

onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll)
})
</script>

<template>
  <Transition name="fade">
    <button
      v-show="visible"
      class="back-to-top"
      @click="scrollToTop"
      aria-label="返回顶部"
    >
      <svg viewBox="0 0 36 36" class="progress-ring">
        <circle
          class="progress-ring-bg"
          cx="18"
          cy="18"
          r="16"
        />
        <circle
          class="progress-ring-progress"
          cx="18"
          cy="18"
          r="16"
          :stroke-dasharray="`${progress * 100} 100`"
        />
      </svg>
      <svg class="arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
        <polyline points="18 15 12 9 6 15"></polyline>
      </svg>
    </button>
  </Transition>
</template>

<style scoped>
.back-to-top {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--vp-c-bg);
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 99;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.progress-ring {
  position: absolute;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.progress-ring-bg {
  fill: none;
  stroke: var(--vp-c-divider);
  stroke-width: 2;
}

.progress-ring-progress {
  fill: none;
  stroke: var(--vp-c-brand-1);
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke-dasharray 0.1s;
}

.arrow {
  position: relative;
  color: var(--vp-c-text-1);
}

.back-to-top:hover .arrow {
  color: var(--vp-c-brand-1);
}
</style>

可配置选项

vue
<script setup lang="ts">
interface Props {
  threshold?: number    // 显示阈值
  bottom?: string       // 底部距离
  right?: string        // 右侧距离
  size?: 'sm' | 'md' | 'lg'  // 按钮大小
}

const props = withDefaults(defineProps<Props>(), {
  threshold: 300,
  bottom: '2rem',
  right: '2rem',
  size: 'md'
})

const sizeMap = {
  sm: '36px',
  md: '44px',
  lg: '52px'
}
</script>

<template>
  <button
    class="back-to-top"
    :style="{
      bottom: props.bottom,
      right: props.right,
      width: sizeMap[props.size],
      height: sizeMap[props.size]
    }"
  >
    <!-- ... -->
  </button>
</template>

使用示例

vue
<!-- 使用默认配置 -->
<BackToTop />

<!-- 自定义配置 -->
<BackToTop
  :threshold="500"
  bottom="3rem"
  right="1.5rem"
  size="lg"
/>

参考链接

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献