選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

127 行
3.0 KiB

  1. <template>
  2. <Teleport to="body">
  3. <Transition name="b2t">
  4. <button
  5. v-if="visible"
  6. type="button"
  7. aria-label="回到顶部"
  8. title="回到顶部"
  9. class="b2t-btn"
  10. @click="toTop"
  11. >
  12. <UIcon name="i-lucide-arrow-up" class="size-5" />
  13. </button>
  14. </Transition>
  15. </Teleport>
  16. </template>
  17. <script setup>
  18. /**
  19. * 全局「回到顶部」按钮。
  20. * 挂在 layouts/default.vue 里,所有页面共用一份,不需要各页面单独引入。
  21. *
  22. * 几个取舍:
  23. * - 滚动超过一屏(600px)才出现,避免短页面上多一个悬浮元素;
  24. * - z-index 取 90,压在内容之上、但低于登录弹窗(120)、详情弹窗(120)、历史抽屉(110)、对比弹层(100),
  25. * 弹层打开时不会盖在上面;
  26. * - 移动端工具页底部有固定操作条(z-50,高约 100px),所以小屏下按钮上移,避免互相遮挡;
  27. * - 系统开启「减少动效」时直接跳转,不做平滑滚动。
  28. */
  29. const SHOW_OFFSET = 600
  30. const visible = ref(false)
  31. let ticking = false
  32. function onScroll() {
  33. // 滚动事件用 rAF 节流,避免每帧都读 scrollY 触发重排
  34. if (ticking) return
  35. ticking = true
  36. requestAnimationFrame(() => {
  37. visible.value = window.scrollY > SHOW_OFFSET
  38. ticking = false
  39. })
  40. }
  41. function prefersReducedMotion() {
  42. return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
  43. }
  44. function toTop() {
  45. window.scrollTo({ top: 0, behavior: prefersReducedMotion() ? 'auto' : 'smooth' })
  46. }
  47. onMounted(() => {
  48. onScroll()
  49. window.addEventListener('scroll', onScroll, { passive: true })
  50. })
  51. onUnmounted(() => window.removeEventListener('scroll', onScroll))
  52. </script>
  53. <style scoped>
  54. /*
  55. 用 CSS 变量而不是 Tailwind 类,保证 Teleport 到 body 后仍带上 scoped 属性选择器时样式生效,
  56. 颜色全部走主题变量,暗色模式自动适配。
  57. */
  58. .b2t-btn {
  59. position: fixed;
  60. right: 24px;
  61. bottom: 112px;
  62. z-index: 90;
  63. display: flex;
  64. align-items: center;
  65. justify-content: center;
  66. width: 44px;
  67. height: 44px;
  68. border: 1px solid var(--c-e8eef7);
  69. border-radius: 9999px;
  70. color: var(--c-5b6b80);
  71. background: var(--c-ffffff);
  72. box-shadow: 0 8px 24px var(--sh-23-32-51-140);
  73. cursor: pointer;
  74. transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
  75. }
  76. /* 桌面端底部没有固定操作条,可以贴近底部 */
  77. @media (min-width: 1024px) {
  78. .b2t-btn {
  79. right: 32px;
  80. bottom: 32px;
  81. width: 48px;
  82. height: 48px;
  83. }
  84. }
  85. .b2t-btn:hover {
  86. color: var(--c-0b8cff);
  87. border-color: var(--c-0b8cff);
  88. background: var(--c-eef7ff);
  89. }
  90. .b2t-btn:focus-visible {
  91. outline: 2px solid var(--c-0b8cff);
  92. outline-offset: 2px;
  93. }
  94. .b2t-enter-active,
  95. .b2t-leave-active {
  96. transition: opacity 0.2s ease, transform 0.2s ease;
  97. }
  98. .b2t-enter-from,
  99. .b2t-leave-to {
  100. opacity: 0;
  101. transform: translateY(8px);
  102. }
  103. @media (prefers-reduced-motion: reduce) {
  104. .b2t-btn,
  105. .b2t-enter-active,
  106. .b2t-leave-active {
  107. transition: none;
  108. }
  109. .b2t-enter-from,
  110. .b2t-leave-to {
  111. transform: none;
  112. }
  113. }
  114. </style>