掘金文章归档 · 第 84 篇 Vue3 组件 · 导航栏

可适配翻页导航栏实现(vue3)

scrollWidth / clientWidth / scrollLeft 判断溢出,平滑滚动模拟翻页

原文作者:随意_(掘金) | juejin.cn/post/7384636920918376458 | 发布于 2024-06-26 | 阅读 698 · 约 4 分钟

0背景

横向导航栏菜单过多的时候,会造成横向的导航栏隐藏。现想做:当导航栏过多隐藏的时候,加一个【下一页】;当左边有隐藏的时候,加一个【上一页】。效果如下:

  1. 导航过多,则展示"下一页";
  2. 点击了"下一页",则左右有隐藏后,展示"上一页";
  3. 最终实现效果如下。
截图占位:导航过多展示下一页按钮 → 点击翻页后同时展示上一页/下一页

1实现思路

dom 元素中有 scrollWidth、clientWidth、scrollLeft 属性。在 DOM 中,这三个是与元素滚动和尺寸相关的属性,特别是在处理滚动容器或可滚动元素时非常有用:

scrollWidth

返回元素的完整宽度,包括由于溢出(overflow)而被隐藏的部分。通常等于元素可见宽度加上任何由于内容过多而溢出到视口之外的部分,用于确定元素的实际内容宽度。

clientWidth

返回元素的内部宽度(以像素为单位),包括元素的填充(padding),但不包括边框(border)、外边距(margin)和水平滚动条。通常是元素在视口中可见的宽度。

scrollLeft

设置或返回元素被滚动到左边的距离(以像素为单位)。这是一个可写属性,可以设置它来滚动元素到特定位置,也可读取来确定当前滚动位置。常用于滚动动画、滚动到特定位置或跟踪滚动位置。

属性含义
clientWidth可视窗口宽度
scrollWidth全部内容的宽度
scrollLeft元素距离左右的滚动距离

判断逻辑:

  1. 判断 dom.scrollWidth > dom.clientWidth + dom.scrollLeft 来判断当前元素是否被隐藏,隐藏了则证明右侧有内容被遮盖,这时"下一页"按钮需要展示。为避免临界值,可以增加固定宽度来辅助校验,如 dom.scrollWidth > dom.clientWidth + dom.scrollLeft + 80(80 为辅助校验值,处理 dom.clientWidth + dom.scrollLeft 已到最右边但仍小于 scrollWidth 的精度问题)。
  2. 判断 dom.scrollLeft 是否是 0,不为 0 则是左右有隐藏,需要展示"上一页"按钮。
  3. 每次点击前后的操作时,dom.scrollLeft += 120 或者 dom.scrollLeft -= 120,更改左侧间距。
  4. js 动态设置 dom.scrollLeft 并不会触发 css 的 transition 过渡,需要增加一个函数,来模拟滚动的平滑滚动。

2完整代码

代码结构如下,借助 vue3 来实现 demo,源码如下:

<template>
  <div class="header">
    <div class="prev btn" @click="changeLeft" v-if="prevable">前</div>
    <div class="menu-wrap" ref="wrapRef">
      <!-- 导航 -->
      <div class="menu-item" v-for="(item, index) in nemuList" :key="index">{{ item }}</div>
    </div>
    <div class="next btn" @click="changeRight" v-if="nextable">后</div>
    <div class="logo">
      头像等占位符
    </div>
  </div>
</template>
<script setup lang="ts">
import { onMounted, ref, nextTick } from "vue"

// 导航
const nemuList = ref(Array.from({ length: 20 }, (_, index) => `测试导航${index + 1}`));
// 上一页是否可点击
const prevable = ref(false);
// 下一页是否可点击
const nextable = ref(false);
// dom
const wrapRef = ref();

const changeLeft = async () => {
  if (!prevable.value) return;
  // 每次翻页一个
  const dom = wrapRef.value;
  smoothScrollTo(dom, dom.scrollLeft - 120, 300)
  setTimeout(async () => {
    await nextTick();
    init()
  }, 300);
}

const changeRight = async () => {
  if (!nextable.value) return;
  // 每次翻页一个
  const dom = wrapRef.value;
  smoothScrollTo(dom, dom.scrollLeft + 120, 300)
  setTimeout(async () => {
    await nextTick();
    init()
  }, 300);
}

const init = () => {
  const dom = wrapRef.value;
  if (!dom) return;
  if (dom.scrollWidth > dom.clientWidth + dom.scrollLeft + 80) {
    nextable.value = true;
  } else {
    nextable.value = false;
  }
  // 判断是否有上一页面
  if (dom.scrollLeft) {
    prevable.value = true;
  } else {
    prevable.value = false;
  }
}

onMounted(() => {
  init()
})

// 定义一个函数来实现平滑滚动
const smoothScrollTo = (element: any, targetScrollLeft: any, duration: any) => {
  // 初始滚动位置
  let start = element.scrollLeft;
  let startTime: number | null = null;

  // 动画效果的定时器
  function scroll() {
    // 确保startTime被设置
    if (!startTime) startTime = performance.now();

    // 计算经过的时间
    const currentTime = performance.now();
    const timeElapsed = currentTime - startTime;

    // 计算当前应该滚动到的位置
    let progress = Math.min(timeElapsed / duration, 1);

    // 使用ease-out立方贝塞尔曲线公式
    element.scrollLeft = start + ((targetScrollLeft - start) * (1 - Math.pow(1 - progress, 3)));

    // 如果动画没有结束,则继续动画
    if (timeElapsed < duration) {
      requestAnimationFrame(scroll);
    }
  }

  // 开始滚动动画
  requestAnimationFrame(scroll);
}
</script>

<style scoped lang="scss">
* {
  margin: 0;
  padding: 0;
}

.header {
  height: 50px;
  width: 100%;
  border: 1px solid gray;
  display: flex;

  .btn {
    width: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px solid salmon;
    border-radius: 50%;
    cursor: pointer;

    &:hover {
      color: white;
      background-color: blue;
    }
  }

  .menu-wrap {
    flex: 1;
    overflow: hidden;
    display: flex;
    transition: all 0.4s ease-in-out;

    .menu-item {
      transition: all 1s ease-in-out;
      width: 120px;
      display: flex;
      justify-content: center;
      align-items: center;
      border: 1px solid greenyellow;
      flex-shrink: 0;
    }
  }

  .logo {
    width: 200px;
    background: orange;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
  }
}
</style>
📌 核心:scrollWidth > clientWidth + scrollLeft + 80 控制"下一页",scrollLeft !== 0 控制"上一页",requestAnimationFrame + ease-out 曲线实现平滑滚动。