Dream主题文章页返回顶部

星期六, 2月 14, 2026 | 5分钟阅读 | 更新于 星期六, 2月 14, 2026

DOM_ZS

🚀 返回顶部按钮 - 完整实现方案

针对 Hugo + Dream 主题 的完整返回顶部按钮实现,固定悬浮在页面右下角,支持滚动显示/隐藏、平滑滚动、深浅色适配。


📁 Partial 组件(推荐)

1️⃣ 创建 layouts/partials/back-to-top.html

{{/* 
  返回顶部按钮 - Dream 主题兼容版
  功能:动态创建、固定右下角、滚动显示/隐藏、平滑滚动、深色模式、键盘辅助
  配置:hugo.toml 中 params.backToTop
*/}}
{{ if .Site.Params.enableBackToTop }}
<script>
(function() {
  // ========== 配置 ==========
  const config = {
    // 显示阈值:滚动超过多少像素显示按钮
    showThreshold: {{ .Site.Params.backToTop.showThreshold | default 300 }},
    
    // 滚动动画时长(毫秒)
    scrollDuration: {{ .Site.Params.backToTop.scrollDuration | default 600 }},
    
    // 按钮位置
    position: {{ .Site.Params.backToTop.position | default "bottom-right" }}, // bottom-right | bottom-left
    
    // 是否显示进度环(需额外 CSS)
    showProgress: {{ .Site.Params.backToTop.showProgress | default false }},
    
    // 主题颜色变量(适配 Dream 主题)
    colors: {
      bg: 'hsl(var(--p))',
      text: 'hsl(var(--pc))',
      border: 'hsl(var(--p) / 0.2)',
      hoverBg: 'hsl(var(--p) / 0.9)',
    }
  };

  // ========== 创建按钮 ==========
  function createButton() {
    // 检查是否已存在
    if (document.getElementById('back-to-top')) return;
    
    const btn = document.createElement('button');
    btn.id = 'back-to-top';
    btn.setAttribute('aria-label', '返回顶部');
    btn.setAttribute('title', '按 Home 键也可返回顶部');
    btn.setAttribute('tabindex', '0');
    
    // 位置样式
    const positionStyles = config.position === 'bottom-left' 
      ? 'bottom: 24px !important; left: 24px !important; right: auto !important;'
      : 'bottom: 24px !important; right: 24px !important; left: auto !important;';
    
    // 应用样式(内联 + !important 确保优先级)
    btn.style.cssText = `
      position: fixed !important;
      ${positionStyles}
      width: 48px !important;
      height: 48px !important;
      border-radius: 50% !important;
      background-color: ${config.colors.bg} !important;
      color: ${config.colors.text} !important;
      border: 2px solid ${config.colors.border} !important;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease !important;
      z-index: 999999 !important;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      cursor: pointer !important;
      -webkit-tap-highlight-color: transparent !important;
      user-select: none !important;
    `;
    
    // 图标 SVG
    btn.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" 
           class="w-6 h-6 flex-shrink-0" 
           fill="none" 
           viewBox="0 0 24 24" 
           stroke="currentColor"
           stroke-width="2"
           style="pointer-events: none; transition: transform 0.2s ease;">
        <path stroke-linecap="round" 
              stroke-linejoin="round" 
              d="M5 10l7-7m0 0l7 7m-7-7v18" />
      </svg>
    `;
    
    // 添加到 body(避免被父元素裁剪)
    document.body.appendChild(btn);
    
    // 初始化状态
    btn.style.transform = 'translateY(20px) scale(0.9)';
    
    return btn;
  }

  // ========== 滚动检测 ==========
  function setupScrollHandler(btn) {
    let ticking = false;
    const threshold = config.showThreshold;
    
    function checkScroll() {
      const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
      
      if (scrollY > threshold) {
        // 显示按钮
        btn.style.opacity = '1';
        btn.style.pointerEvents = 'auto';
        btn.style.transform = 'translateY(0) scale(1)';
      } else {
        // 隐藏按钮
        btn.style.opacity = '0';
        btn.style.pointerEvents = 'none';
        btn.style.transform = 'translateY(20px) scale(0.9)';
      }
    }
    
    // 初始检查(防止页面加载时已在底部)
    checkScroll();
    
    // 滚动监听(passive 优化性能)
    window.addEventListener('scroll', function() {
      if (!ticking) {
        window.requestAnimationFrame(function() {
          checkScroll();
          ticking = false;
        });
        ticking = true;
      }
    }, { passive: true });
    
    return checkScroll;
  }

  // ========== 事件绑定 ==========
  function bindEvents(btn) {
    // 点击返回顶部
    btn.addEventListener('click', function(e) {
      e.preventDefault();
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    });
    
    // 键盘支持:Enter/Space 触发,Home 键直接返回顶部
    btn.addEventListener('keydown', function(e) {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        window.scrollTo({ top: 0, behavior: 'smooth' });
      }
    });
    
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Home' && btn.style.pointerEvents !== 'none') {
        e.preventDefault();
        window.scrollTo({ top: 0, behavior: 'smooth' });
      }
    });
    
    // 悬停动画
    btn.addEventListener('mouseenter', function() {
      this.style.transform = 'translateY(-2px) scale(1.1)';
      this.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)';
      this.style.backgroundColor = config.colors.hoverBg;
      
      // 图标微动画
      const icon = this.querySelector('svg');
      if (icon) icon.style.transform = 'translateY(-2px)';
    });
    
    btn.addEventListener('mouseleave', function() {
      this.style.transform = 'translateY(0) scale(1)';
      this.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
      this.style.backgroundColor = config.colors.bg;
      
      const icon = this.querySelector('svg');
      if (icon) icon.style.transform = 'translateY(0)';
    });
    
    // 触摸设备优化
    btn.addEventListener('touchstart', function() {
      this.style.transform = 'scale(0.95)';
    }, { passive: true });
    
    btn.addEventListener('touchend', function() {
      this.style.transform = 'scale(1)';
    }, { passive: true });
  }

  // ========== 深色模式适配 ==========
  function setupThemeObserver(btn) {
    // 初始适配
    function adaptTheme() {
      const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
                    window.matchMedia('(prefers-color-scheme: dark)').matches;
      
      btn.style.backgroundColor = isDark 
        ? 'hsl(var(--p) / 0.85) !important' 
        : config.colors.bg;
    }
    
    adaptTheme();
    
    // 监听主题变化
    const observer = new MutationObserver(adaptTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme']
    });
    
    // 监听系统偏好变化
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', adaptTheme);
  }

  // ========== 移动端适配 ==========
  function setupResponsive(btn) {
    function adaptSize() {
      if (window.innerWidth <= 640) {
        btn.style.width = '44px';
        btn.style.height = '44px';
        btn.style.bottom = '16px';
        btn.style.right = config.position === 'bottom-left' ? '16px' : '16px';
      } else {
        btn.style.width = '48px';
        btn.style.height = '48px';
        btn.style.bottom = '24px';
        btn.style.right = config.position === 'bottom-left' ? '24px' : '24px';
      }
    }
    
    adaptSize();
    window.addEventListener('resize', adaptSize, { passive: true });
  }

  // ========== 初始化 ==========
  function init() {
    const btn = createButton();
    if (!btn) return;
    
    setupScrollHandler(btn);
    bindEvents(btn);
    setupThemeObserver(btn);
    setupResponsive(btn);
    
    // 调试日志(生产环境可移除)
    if (window.location.hostname === 'localhost') {
      console.log('✅ Back-to-top button initialized');
    }
  }

  // 等待 DOM 就绪
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();
</script>
{{ end }}

2️⃣ 在 hugo.toml 中启用

[params]
  # 启用返回顶部按钮
  enableBackToTop = true

  # 可选:自定义配置
  [params.backToTop]
    # 滚动超过多少像素显示按钮(默认 300)
    showThreshold = 300
    
    # 平滑滚动时长(毫秒,默认 600)
    scrollDuration = 600
    
    # 按钮位置:bottom-right | bottom-left(默认 bottom-right)
    position = "bottom-right"
    
    # 是否显示进度环(默认 false,需额外 CSS 支持)
    showProgress = false

3️⃣ 在主题布局中引入

Dream 主题使用 baseof.html 作为基础模板,在 {{ block "main" . }} 之后添加:

覆盖 layouts/_default/baseof.html

# 复制主题的 baseof.html 到项目
cp themes/Dream/layouts/_default/baseof.html layouts/_default/baseof.html

然后在 </body> 标签前添加:

<!-- layouts/_default/baseof.html -->
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}" data-theme="{{ .Site.Params.defaultTheme }}">
<head>
  {{/* ... 原有 head 内容 ... */}}
</head>
<body class="flex flex-col min-h-screen">
  {{/* ... 原有 body 内容 ... */}}
  
  <main class="flex-grow">
    {{ block "main" . }}{{ end }}
  </main>
  
  {{/* ... 原有 footer 等内容 ... */}}
  
  {{/* 👇 添加返回顶部按钮 */}}
  {{ partial "back-to-top.html" . }}
  
</body>
</html>

🚀 快速启用(5 分钟)

# 1. 创建 partial 文件
mkdir -p layouts/partials
cat > layouts/partials/back-to-top.html << 'EOF'
上述代码
EOF

# 2. 在 hugo.toml 中启用
echo -e "\n[params]\n  enableBackToTop = true" >> hugo.toml

# 3. 在 layouts/_default/baseof.html 的 </body> 前添加
#    {{ partial "back-to-top.html" . }}

# 4. 清理缓存并重启
hugo --gc --ignoreCache
hugo server -D --disableFastRender

✅ 最终效果

📱 初始状态(滚动 <300px):
                    (按钮隐藏)

📱 滚动后(>300px):
                    ┌─────────┐
                    │    ↑    │  ← 圆形按钮,主题色
                    └─────────┘

🖱️ 悬停时:
                    ┌─────────┐
                    │    ↑    │  ← 轻微放大 + 阴影
                    └─────────┘
                       ~~~      ← 阴影增强

🌓 深色模式:
                    ┌─────────┐
                    │    ↑    │  ← 自动适配深色背景
                    └─────────┘

📱 移动端 (<640px):
              ┌─────┐
              │  ↑  │  ← 按钮稍小,位置内缩
              └─────┘
  • 📱 移动端自动调整位置
  • 🌓 深浅色模式自动适配
  • ⚡ 性能优化(requestAnimationFrame)
  • ♿ 支持键盘操作和屏幕阅读器
  • 🎨 平滑动画过渡

🔧 可选增强

1️⃣ 添加进度环(显示页面滚动进度)

btn.innerHTML 中添加 SVG 环:

btn.innerHTML = `
  <svg class="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 36 36">
    <path class="text-primary/20" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831" 
          fill="none" stroke="currentColor" stroke-width="3"/>
    <path id="progress-ring" class="text-primary" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831" 
          fill="none" stroke="currentColor" stroke-width="3" 
          stroke-dasharray="100, 100" stroke-dashoffset="100"/>
  </svg>
  <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 relative z-10" ...>
    <path ... d="M5 10l7-7m0 0l7 7m-7-7v18" />
  </svg>
`;

// 在 checkScroll 函数中添加进度更新
const progress = Math.min(100, (scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
const ring = btn.querySelector('#progress-ring');
if (ring) ring.style.strokeDashoffset = 100 - progress;

2️⃣ 添加点击音效(可选)

btn.addEventListener('click', function() {
  // 轻微点击反馈
  this.style.transform = 'scale(0.9)';
  setTimeout(() => {
    this.style.transform = 'translateY(0) scale(1)';
  }, 150);
  
  window.scrollTo({ top: 0, behavior: 'smooth' });
});

3️⃣ 添加统计(记录使用次数)

// 在 localStorage 中记录点击次数
btn.addEventListener('click', function() {
  const count = parseInt(localStorage.getItem('backToTopClicks') || '0') + 1;
  localStorage.setItem('backToTopClicks', count);
  console.log(`📊 返回顶部已使用 ${count} 次`);
  window.scrollTo({ top: 0, behavior: 'smooth' });
});

⚠️ 常见问题排查

问题可能原因解决方案
按钮不显示enableBackToTop 未启用检查 hugo.toml 配置
位置不对CSS 被覆盖确认 !importantz-index
点击无效事件未绑定检查 Console 是否有 JS 错误
深色模式不生效主题变量未加载确认 Dream 主题已正确初始化
移动端错位position: fixed 被 transform 影响确保按钮直接添加到 document.body
滚动卡顿事件监听未优化确认使用 passive: truerequestAnimationFrame

✅ 最终验证

访问 http://localhost:1313/任意文章,滚动页面:

1️⃣ 滚动 >300px → 右下角出现圆形按钮
2️⃣ 悬停按钮 → 轻微放大 + 阴影增强
3️⃣ 点击按钮 → 平滑滚动到顶部
4️⃣ 按 Home 键 → 同样返回顶部
5️⃣ 切换深色模式 → 按钮颜色自动适配
6️⃣ 手机访问 → 按钮大小位置自适应

© 2024 - 2026 我的梦境博客

🌱 Powered by Hugo with theme Dream.

关于我

👋 你好,我是DOM_zs!

这是我的个人博客。

📧 联系我:zhangsong192@proton.me

社交链接

© 2024 - 2026 我的梦境博客

🌱 Powered by Hugo with theme Dream.