Vue仿B站移动端导航栏

157 阅读2分钟

Vue仿B站移动端导航栏

使用vue仿写B站移动端导航栏,实现了导航栏的拖拽滚动和切换选项自动滚动等功能。

效果

20240717_220037.gif

代码

<template>
  <div class="switcher-header">
    <div class="switcher-header-after">
      <n-icon size="22" color="#aaa" class="icon">
        <down-icon></down-icon>
      </n-icon>
    </div>
    <div class="tabs-wrap">
      <ul ref="tabsRef" class="tabs-list header-start"
        :style="`transform: translateX(${translateX}px);${transitionDuration}`">
        <li class="tabs-item" :class="item.partitionId === currentTab ? 'tabs-active' : ''"
          v-for="(item, index) in partitionList" :ref="el => setRefMap(index, el)" @click="partitionClick(index)">{{
            item.name }}</li>
        <div class="switcher-header-anchor"
          :style="`width: ${tabAnchorWidth}px;transform: translateX(${tabAnchorLeft}px)`"></div>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeMount, nextTick } from "vue";
import { NIcon } from "naive-ui";
import { Down as DownIcon } from "@icon-park/vue-next";

const partitionList = [
  { name: "首页", partitionId: 1 },
  { name: "动画", partitionId: 2 },
  { name: "番剧", partitionId: 3 },
  { name: "国创", partitionId: 4 },
  { name: "音乐", partitionId: 5 },
  { name: "舞蹈", partitionId: 6 },
  { name: "游戏", partitionId: 7 },
  { name: "知识", partitionId: 8 },
  { name: "科技", partitionId: 9 },
  { name: "运动", partitionId: 10 },
  { name: "汽车", partitionId: 11 },
]

const currentTab = ref(-1);
const tabAnchorLeft = ref(0);
const tabAnchorWidth = ref(0);

const refMap = new Map<number, any>();
const tabsRef = ref<HTMLElement | null>(null);
const setRefMap = (key: number, el: any) => {
  refMap.set(key, el);
}

// 滑动滑动条
const slidingSlider = () => {
  if (tabsRef.value) {
    // PC端
    tabsRef.value.onmousedown = function (e) {
      let startX = e.clientX;
      document.onmousemove = function (e) {
        sliderValueChange(e, startX);
        startX = e.clientX;
      };
      document.onmouseup = function () {
        document.onmousemove = document.onmouseup = null;
      };
    };
    //移动端
    tabsRef.value.ontouchstart = function (e) {
      let startX = e.changedTouches[0].clientX;
      document.ontouchmove = function (e) {
        sliderValueChange(e, startX);
        startX = e.changedTouches[0].clientX;

      };
      document.ontouchend = function () {
        document.ontouchmove = document.ontouchend = null;
      };
    };
  }
}

//滑动条值改变
const translateX = ref(0);
const transitionDuration = ref("");
const sliderValueChange = (e: MouseEvent | TouchEvent, startX: number) => {
  let clientX: number;
  if (e instanceof MouseEvent) {
    clientX = e.clientX;
  } else {
    clientX = e.changedTouches[0].clientX;
  }

  translateX.value += (clientX - startX);
  startX = clientX;

  translateX.value = Math.min(translateX.value, 0);
  if (tabsRef.value) {
    transitionDuration.value = "";
    translateX.value = Math.max(translateX.value, tabsRef.value.clientWidth - tabsRef.value.scrollWidth);
  }
}

const partitionClick = (index: number) => {
  const tabDom = refMap.get(index);
  tabAnchorWidth.value = tabDom.clientWidth - 20;
  tabAnchorLeft.value = tabDom.offsetLeft + 10;

  let totalWidth = 0;
  if (index + 1 < partitionList.length) {
    for (let i = 0; i <= index + 1; i++) {
      totalWidth += refMap.get(i).clientWidth;
    }

    if (tabsRef.value?.clientWidth) {
      const offset = -(totalWidth - tabsRef.value.clientWidth);
      if (offset <= 0) {
        translateX.value = offset;
        transitionDuration.value = "transition-duration: 300ms;"
      }
    }
  }

  currentTab.value = partitionList[index].partitionId
}

onBeforeMount(async () => {
  // TODO: 从后端加载分区信息

  nextTick(() => {
    partitionClick(0)
  })
})

onMounted(() => {
  slidingSlider();
});
</script>

<style lang="scss" scoped>
.switcher-header {
  height: 46px;
  position: relative;
  box-sizing: border-box;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  overflow: hidden;
  background-color: #fff;

  &::before {
    content: "";
    position: absolute;
    box-sizing: border-box;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    transform: scaleY(.5);
    background-color: #e7e7e7;
  }

  .switcher-header-after {
    float: right;
    margin-top: 6px;
    order: 2;

    .icon {
      margin: 0 10px;
    }
  }

  .tabs-wrap {
    flex: 1;
    height: 100%;
    overflow: hidden;
  }

  .tabs-list {
    position: relative;
    font-size: 0;
    z-index: 1;
    padding: 0;
    margin: 0;
    flex: 1;
    white-space: nowrap;

    .tabs-item {
      font-size: 15px;
      flex-shrink: 0;
      height: 46px;
      line-height: 46px;
      display: inline-block;
      text-align: center;
      vertical-align: middle;
      white-space: nowrap;
      -webkit-user-select: none;
      -moz-user-select: none;
      user-select: none;
      padding: 0 16px;
    }

    .switcher-header-anchor {
      display: block;
      position: absolute;
      left: 0;
      bottom: 0;
      height: .53333vmin;
      border-radius: .53333vmin;
      transition-timing-function: cubic-bezier(.645, .045, .355, 1);
      transition-property: width, height, transform;
      pointer-events: none;
      background: var(--primary-color);
      transition-duration: 300ms;
    }
  }

  .header-start {
    text-align: left;
    display: flex;
    flex-direction: row;
  }
}

.tabs-active {
  color: var(--primary-color);
}
</style>