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>
相关文章
|
9天前
|
资源调度 JavaScript 前端开发
Pinia 如何在 Vue 3 项目中进行安装和配置?
Pinia 如何在 Vue 3 项目中进行安装和配置?
|
4月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
151 64
|
4月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
197 64
|
4月前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
187 60
|
4月前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
159 58
|
4月前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
136 56
|
2月前
|
资源调度 JavaScript 前端开发
创建vue3项目步骤以及安装第三方插件步骤【保姆级教程】
这是一篇关于创建Vue项目的详细指南,涵盖从环境搭建到项目部署的全过程。
286 1
|
3月前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
176 3
|
4月前
|
JavaScript 前端开发 API
从Vue 2到Vue 3的演进
从Vue 2到Vue 3的演进
109 17
|
4月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
134 17

热门文章

最新文章