Vue3回到顶部(BackTop)

76 篇文章 4 订阅
73 篇文章 3 订阅

效果如下图:在线预览

在这里插入图片描述

APIs

BackTop

参数说明类型默认值
icon自定义图标slotundefined
description文字描述string | slotundefined
tooltip文字提示内容string | slotundefined
tooltipPropsTooltip 组件属性配置,参考 Tooltip Propsobject{}
type设置按钮类型‘default’ | ‘primary’‘default’
shape设置按钮形状‘circle’ | ‘square’‘circle’
bottomBackTop 距离页面底部的高度,单位 pxnumber | string40
rightBackTop 距离页面右侧的宽度,单位 pxnumber | string40
zIndex设置 BackTopz-indexnumber9
visibilityHeight滚动时触发显示回到顶部的高度,单位 pxnumber180
toBackTop 渲染的容器节点,可选:元素标签名(例如 body)或者元素本身,下同string | HTMLElement‘body’
listenTo监听滚动的元素,如果为 undefined 会监听距离最近的一个可滚动的祖先节点string | HTMLElementundefined

Events

名称说明类型
click点击按钮的回调函数() => void
show是否展现的回调函数(show: boolean) => void

创建回到顶部组件BackTop.vue

其余引入使用了以下组件和工具函数:

<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import type { Slot } from 'vue'
import Tooltip from '../tooltip'
import { useSlotsExist } from '../utils'
interface Props {
  icon?: Slot // 自定义图标 slot
  description?: string // 文字描述 string | slot
  tooltip?: string // 文字提示内容 string | slot
  tooltipProps?: object // Tooltip 组件属性配置,参考 Tooltip Props
  type?: 'default' | 'primary' // 设置按钮类型
  shape?: 'circle' | 'square' // 设置按钮形状
  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>(), {
  icon: undefined,
  description: undefined,
  tooltip: undefined,
  tooltipProps: () => ({}),
  type: 'default',
  shape: 'circle',
  bottom: 40,
  right: 40,
  zIndex: 9,
  visibilityHeight: 180,
  to: 'body',
  listenTo: undefined
})
const backtopRef = ref<HTMLElement | null>(null)
const scrollTop = ref<number>(0) // 滚动距离
const scrollTarget = ref<HTMLElement | null>(null) // 滚动目标元素
const targetElement = ref<HTMLElement | null>(null) // 渲染容器元素
const emits = defineEmits(['click', 'show'])
const slotsExist = useSlotsExist(['tooltip', 'icon', 'description'])
// 观察器的配置
const config = { childList: true, attributes: true, subtree: true }
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(() => {
  scrollTop.value = scrollTarget.value?.scrollTop ?? 0
})
const backTopStyle = computed(() => {
  return {
    bottom: typeof props.bottom === 'number' ? props.bottom + 'px' : props.bottom,
    right: typeof props.right === 'number' ? props.right + 'px' : props.right,
    zIndex: props.zIndex
  }
})
const backTopShow = computed(() => {
  return scrollTop.value >= props.visibilityHeight
})
const showTooltip = computed(() => {
  return slotsExist.tooltip || props.tooltip
})
const showDescription = computed(() => {
  return slotsExist.description || props.description
})
watch(
  () => props.listenTo,
  () => {
    observer.disconnect()
    removeEventListener()
    observeScroll()
  },
  {
    flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
  }
)
watch(
  () => props.to,
  () => {
    appendBackTop()
  },
  {
    flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
  }
)
watch(backTopShow, (to) => {
  emits('show', to)
})
onMounted(() => {
  observeScroll()
  appendBackTop()
})
onBeforeUnmount(() => {
  observer.disconnect() // 停止观察
  removeEventListener()
  backtopRef.value?.remove()
})
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', scrollEvent)
    window.removeEventListener('resize', resizeEvent)
  }
}
function observeScroll() {
  // 监听滚动的元素
  if (props.listenTo === undefined) {
    scrollTarget.value = getScrollParentElement(backtopRef.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', scrollEvent)
    window.addEventListener('resize', resizeEvent)
  }
}
function appendBackTop() {
  // 渲染容器节点
  if (typeof props.to === 'string') {
    targetElement.value = document.getElementsByTagName(props.to)[0] as HTMLElement
  } else if (props.to instanceof HTMLElement) {
    targetElement.value = props.to
  }
  targetElement.value?.appendChild(backtopRef.value!) // 保证 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="backTopShow" ref="backtopRef" class="m-backtop-wrap" :style="backTopStyle" @click="onBackTop">
      <Tooltip style="border-radius: 22px" :content-style="{ borderRadius: '22px' }" v-bind="tooltipProps">
        <template v-if="showTooltip" #tooltip>
          <slot name="tooltip">{{ tooltip }}</slot>
        </template>
        <div class="m-backtop" :class="`backtop-${type} backtop-${shape}`">
          <slot>
            <span class="backtop-icon" :class="{ 'icon-description': showDescription }">
              <slot name="icon">
                <svg
                  width="1em"
                  height="1em"
                  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>
              </slot>
            </span>
            <span v-if="showDescription" class="backtop-description">
              <slot name="description">{{ description }}</slot>
            </span>
          </slot>
        </div>
      </Tooltip>
    </div>
  </Transition>
</template>
<style lang="less" scoped>
.zoom-enter-active,
.zoom-leave-active {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.zoom-enter-from,
.zoom-leave-to {
  opacity: 0;
  transform: scale(0.5);
}
.m-backtop-wrap {
  position: fixed;
  z-index: var(--z-index);
  .m-backtop {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 44px;
    min-width: 44px;
    cursor: pointer;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    .backtop-icon {
      display: inline-flex;
      font-size: 26px;
      :deep(svg) {
        pointer-events: none;
        fill: currentColor;
        transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      }
    }
    .icon-description {
      font-size: 24px;
    }
    .backtop-description {
      display: flex;
      align-items: center;
      font-size: 12px;
      font-weight: 500;
      line-height: 16px;
      pointer-events: none;
      transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
  }
  .backtop-default {
    color: rgba(0, 0, 0, 0.88);
    background-color: rgba(255, 255, 255, 0.88);
    box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
    .backtop-icon,
    .backtop-description {
      color: rgba(0, 0, 0, 0.88);
    }
    &:hover {
      color: @themeColor;
      background-color: rgba(255, 255, 255);
      box-shadow: 0 2px 8px 3px rgba(0, 0, 0, 0.12);
      .backtop-icon,
      .backtop-description {
        color: @themeColor;
      }
    }
  }
  .backtop-primary {
    color: #fff;
    background-color: @themeColor;
    box-shadow: 0 2px 8px 0 rgba(9, 88, 217, 0.32);
    &:hover {
      background-color: #4096ff;
      box-shadow: 0 2px 8px 3px rgba(9, 88, 217, 0.32);
    }
  }
  .backtop-circle {
    border-radius: 22px;
  }
  .backtop-square {
    border-radius: 8px;
  }
}
</style>

在要使用的页面引入

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

<script setup lang="ts">
import BackTop from './BackTop.vue'
import { ref } from 'vue'
import { DoubleLeftOutlined, VerticalAlignTopOutlined, ArrowUpOutlined } from '@ant-design/icons-vue'
function onShow(show: boolean) {
  console.log('show', show)
}
const scrollContainer = ref()
</script>
<template>
  <div style="height: 450vh">
    <h1>{{ $route.name }} {{ $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <h3 class="mb10">BackTop 会找到首个可滚动的祖先元素并且监听它的滚动事件</h3>
    <BackTop @show="onShow" />
    <h2 class="mt30 mb10">自定义图标</h2>
    <BackTop :right="100">
      <template #icon>
        <DoubleLeftOutlined :rotate="90" />
      </template>
    </BackTop>
    <BackTop :right="160">
      <template #icon>
        <VerticalAlignTopOutlined />
      </template>
    </BackTop>
    <BackTop :right="220">
      <template #icon>
        <ArrowUpOutlined />
      </template>
    </BackTop>
    <h2 class="mt30 mb10">自定义类型和形状</h2>
    <BackTop shape="square" :right="100" :bottom="100" />
    <BackTop shape="square" description="顶部" :right="160" :bottom="100" />
    <BackTop type="primary" :right="220" :bottom="100" />
    <BackTop type="primary" shape="square" :right="280" :bottom="100" />
    <BackTop type="primary" shape="square" description="顶部" :right="340" :bottom="100" />
    <h2 class="mt30 mb10">文字描述</h2>
    <BackTop description="顶部" :right="100" :bottom="160" />
    <h2 class="mt30 mb10">悬浮提示</h2>
    <BackTop tooltip="回到顶部" :right="160" :bottom="160" />
    <BackTop
      tooltip="回到顶部"
      :tooltip-props="{
        bgColor: '#fff',
        tooltipStyle: {
          padding: '8px 12px',
          borderRadius: '12px',
          fontSize: '16px',
          color: 'rgba(0, 0, 0, 0.88)',
          fontWeight: 500
        }
      }"
      :right="220"
      :bottom="160"
    />
    <h2 class="mt30 mb10">自定义可视高度</h2>
    <h3 class="mb10">自定义滚动时触发显示回到顶部的高度</h3>
    <BackTop :bottom="270" :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="260" :bottom="270">
      <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="330" :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="330" :right="260" :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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值