Vue3回到顶部(BackTop)

简介: 这是一个基于 Vue3 的头像组件库,提供了圆形和方形两种头像形状,并支持自定义大小、图片、图标及字符展示。

效果如下图:在线预览

APIs

BackTop

参数 说明 类型 默认值
bottom BackTop 距离页面底部的高度,单位 px number | string 40
right BackTop 距离页面右侧的宽度,单位 px number | string 40
zIndex 设置 BackTopz-index number 9
visibilityHeight 滚动时触发显示回到顶部的高度,单位 px number 180
to BackTop 渲染的容器节点,可选:元素标签名(例如 body)或者元素本身,下同 string | HTMLElement ‘body’
listenTo 监听滚动的元素,如果为 undefined 会监听距离最近的一个可滚动的祖先节点 string | HTMLElement undefined

Events

事件名称 说明 参数
show 是否展现的回调 (show: boolean) => void

其中引入使用了工具函数:throttle 节流

创建回到顶部组件BackTop.vue

<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { throttle } from '../utils'
interface Props {
  bottom?: number | string // BackTop 距离页面底部的高度,单位 px
  right?: number | string // BackTop 距离页面右侧的宽度,单位 px
  zIndex?: number // 设置 BackTop 的 z-index
  visibilityHeight?: number // 滚动时触发显示回到顶部的高度,单位 px
  to?: string | HTMLElement // BackTop 渲染的容器节点,可选:元素标签名 (例如 'body') 或者元素本身,下同
  listenTo?: string | HTMLElement // 监听滚动的元素,如果为 undefined 会监听距离最近的一个可滚动的祖先节点
}
const props = withDefaults(defineProps<Props>(), {
  bottom: 40,
  right: 40,
  zIndex: 9,
  visibilityHeight: 180,
  to: 'body',
  listenTo: undefined
})
const bottomPosition = computed(() => {
  if (typeof props.bottom === 'number') {
    return props.bottom + 'px'
  }
  return props.bottom
})
const rightPosition = computed(() => {
  if (typeof props.right === 'number') {
    return props.right + 'px'
  }
  return props.right
})
const show = computed(() => {
  return scrollTop.value >= props.visibilityHeight
})
const backtop = ref<HTMLElement | null>(null)
const scrollTop = ref<number>(0)
const scrollTarget = ref<HTMLElement | null>(null)
const target = ref<HTMLElement | null>(null)
const emits = defineEmits(['click', 'show'])
// 观察器的配置
const config = { childList: true, attributes: true, subtree: true }
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(() => {
  scrollTop.value = scrollTarget.value?.scrollTop ?? 0
})
watch(
  () => props.listenTo,
  () => {
    observer.disconnect()
    removeEventListener()
    observeScroll()
  },
  {
    flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
  }
)
watch(
  () => props.to,
  () => {
    insertElement()
  },
  {
    flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
  }
)
watch(show, (to) => {
  emits('show', to)
})
onMounted(() => {
  observeScroll()
  insertElement()
})
onBeforeUnmount(() => {
  observer.disconnect() // 停止观察
  removeEventListener()
  backtop.value?.remove()
})
const throttleScroll = throttle(scrollEvent, 100)
const throttleResize = throttle(resizeEvent, 100)
function scrollEvent(e: Event) {
  scrollTop.value = (e.target as HTMLElement).scrollTop
}
function resizeEvent() {
  scrollTop.value = scrollTarget.value?.scrollTop ?? 0
}
function removeEventListener() {
  // 移除监听事件
  if (scrollTarget.value) {
    scrollTarget.value.removeEventListener('scroll', throttleScroll)
    window.removeEventListener('resize', throttleResize)
  }
}
function observeScroll() {
  // 监听滚动的元素
  if (props.listenTo === undefined) {
    scrollTarget.value = getScrollParentElement(backtop.value?.parentElement)
  } else if (typeof props.listenTo === 'string') {
    scrollTarget.value = document.getElementsByTagName(props.listenTo)[0] as HTMLElement
  } else if (props.listenTo instanceof HTMLElement) {
    scrollTarget.value = props.listenTo
  }
  if (scrollTarget.value) {
    observer.observe(scrollTarget.value, config)
    scrollTarget.value.addEventListener('scroll', throttleScroll)
    window.addEventListener('resize', throttleResize)
  }
}
function insertElement() {
  // 渲染容器节点
  if (typeof props.to === 'string') {
    target.value = document.getElementsByTagName(props.to)[0] as HTMLElement
  } else if (props.to instanceof HTMLElement) {
    target.value = props.to
  }
  target.value?.appendChild(backtop.value as Node) // 保证backtop节点只存在一个
}
function getScrollParentElement(el: any) {
  if (el) {
    if (el.scrollHeight > el.clientHeight) {
      return el
    } else {
      return getScrollParentElement(el.parentElement)
    }
  }
  return null
}
function onBackTop() {
  scrollTarget.value &&
    scrollTarget.value.scrollTo({
      top: 0,
      behavior: 'smooth' // 平滑滚动并产生过渡效果
    })
  emits('click')
}
</script>
<template>
  <Transition name="zoom">
    <div
      v-show="show"
      ref="backtop"
      class="m-backtop"
      :style="`bottom: ${bottomPosition}; right: ${rightPosition}; --z-index: ${zIndex};`"
      @click="onBackTop"
    >
      <slot>
        <span class="m-icon">
          <svg
            class="u-icon"
            viewBox="0 0 24 24"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            xlinkHref="http://www.w3.org/1999/xlink"
          >
            <g stroke="none" stroke-width="1" fill-rule="evenodd">
              <g transform="translate(-139.000000, -4423.000000)" fill-rule="nonzero">
                <g transform="translate(120.000000, 4285.000000)">
                  <g transform="translate(7.000000, 126.000000)">
                    <g
                      transform="translate(24.000000, 24.000000) scale(1, -1) translate(-24.000000, -24.000000) translate(12.000000, 12.000000)"
                    >
                      <g transform="translate(4.000000, 2.000000)">
                        <path
                          d="M8,0 C8.51283584,0 8.93550716,0.38604019 8.99327227,0.883378875 L9,1 L9,10.584 L12.2928932,7.29289322 C12.6834175,6.90236893 13.3165825,6.90236893 13.7071068,7.29289322 C14.0675907,7.65337718 14.0953203,8.22060824 13.7902954,8.61289944 L13.7071068,8.70710678 L8.70710678,13.7071068 L8.62544899,13.7803112 L8.618,13.784 L8.59530661,13.8036654 L8.4840621,13.8753288 L8.37133602,13.9287745 L8.22929083,13.9735893 L8.14346259,13.9897165 L8.03324678,13.9994506 L7.9137692,13.9962979 L7.77070917,13.9735893 L7.6583843,13.9401293 L7.57677845,13.9063266 L7.47929125,13.8540045 L7.4048407,13.8036865 L7.38131006,13.7856883 C7.35030318,13.7612383 7.32077858,13.7349921 7.29289322,13.7071068 L2.29289322,8.70710678 L2.20970461,8.61289944 C1.90467972,8.22060824 1.93240926,7.65337718 2.29289322,7.29289322 C2.65337718,6.93240926 3.22060824,6.90467972 3.61289944,7.20970461 L3.70710678,7.29289322 L7,10.585 L7,1 L7.00672773,0.883378875 C7.06449284,0.38604019 7.48716416,0 8,0 Z"
                        ></path>
                        <path
                          d="M14.9333333,15.9994506 C15.5224371,15.9994506 16,16.4471659 16,16.9994506 C16,17.5122865 15.5882238,17.9349578 15.0577292,17.9927229 L14.9333333,17.9994506 L1.06666667,17.9994506 C0.477562934,17.9994506 0,17.5517354 0,16.9994506 C0,16.4866148 0.411776203,16.0639435 0.9422708,16.0061783 L1.06666667,15.9994506 L14.9333333,15.9994506 Z"
                        ></path>
                      </g>
                    </g>
                  </g>
                </g>
              </g>
            </g>
          </svg>
        </span>
      </slot>
    </div>
  </Transition>
</template>
<style lang="less" scoped>
.zoom-enter-from,
.zoom-leave-to {
  opacity: 0;
  transform: scale(0.5);
}
.m-backtop {
  position: fixed;
  z-index: var(--z-index);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(0, 0, 0, 0.88);
  border-radius: 22px;
  height: 44px;
  min-width: 44px;
  box-shadow: 0 2px 8px 0px rgba(0, 0, 0, 0.12);
  background-color: transparent;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  &:hover {
    color: @themeColor;
    box-shadow: 0 2px 8px 3px rgba(0, 0, 0, 0.12);
    .m-icon .u-icon {
      fill: @themeColor;
    }
  }
  .m-icon {
    font-size: 26px;
    height: 1em;
    width: 1em;
    line-height: 1em;
    text-align: center;
    display: inline-block;
    position: relative;
    transform: translateZ(0);
    .u-icon {
      pointer-events: none;
      height: 1em;
      width: 1em;
      fill: rgba(0, 0, 0, 0.88);
      transition: fill 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
  }
}
</style>

其中引入使用了以下组件:

在要使用的页面引入

<script setup lang="ts">
import BackTop from './BackTop.vue'
import { ref } from 'vue'
function onShow(show: boolean) {
  console.log('show', show)
}
const scrollContainer = ref()
</script>
<template>
  <div style="height: 150vh">
    <h1>{
  { $route.name }} {
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <h3 class="mb10">BackTop 会找到首个可滚动的祖先元素并且监听它的滚动事件</h3>
    <BackTop :right="100" @show="onShow" />
    <h2 class="mt30 mb10">自定义可视高度</h2>
    <h3 class="mb10">自定义滚动时触发显示回到顶部的高度</h3>
    <BackTop :bottom="100" :visibility-height="300">
      <div style="width: 200px; height: 40px; line-height: 40px; text-align: center; font-size: 14px"
        >可视高度:300px</div
      >
    </BackTop>
    <h2 class="mt30 mb10">自定义位置</h2>
    <BackTop :right="40" :bottom="160">
      <div style="width: 200px; height: 40px; line-height: 40px; text-align: center; font-size: 14px">改变位置</div>
    </BackTop>
    <h2 class="mt30 mb10">自定义监听目标</h2>
    <h3 class="mb10">自定义设定监听哪个元素来触发 BackTop</h3>
    <BackTop :listen-to="scrollContainer" :bottom="220" :visibility-height="10">
      <div style="width: 200px; height: 40px; line-height: 40px; text-align: center; font-size: 14px"> 指定目标 </div>
    </BackTop>
    <div ref="scrollContainer" style="width: 600px; overflow: auto; height: 100px; line-height: 1.57">
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
      这块应该写一个有意思的笑话。<br />
    </div>
    <br />
    <h3 class="mb10">自动监听 Scrollbar 来触发 BackTop</h3>
    <Scrollbar style="width: 600px; height: 100px">
      <BackTop :bottom="280" :visibility-height="10">
        <div style="width: 200px; height: 40px; line-height: 40px; text-align: center; font-size: 14px">监听 Scrollbar</div>
      </BackTop>
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
      这块应该写一个有意思的笑话。
      <br />
    </Scrollbar>
  </div>
</template>
相关文章
|
5月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
814 139
|
10月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
1219 5
|
5月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
433 1
|
6月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
709 12
|
5月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
497 0
|
7月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
727 1
|
7月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
412 0
|
8月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
200 0
|
10月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
977 17
|
11月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点

热门文章

最新文章