还不快学?!不到100行代码即可实现前端电梯导航

261 阅读3分钟

最近天气好热,热的懒得打字,中午吃点西瓜补补脑(bushi)🔥🔥

好了回到正题,今天说一下电梯导航。这个场景不复杂,我这次说一个比较简单的实现方法。

正题开始

什么是电梯导航?

坐过电梯的小伙伴都知道,电梯内的按钮对应不同楼层,点击相应按钮就可以到达相应楼层,我们要实现的组件也是这个原理,其实很多App都有类似场景,最典型的就是通讯录列表了,我们做的跟这个也就类似,不过今天我们主要实现pc端里的电梯导航。

演示4.gif

布局

电梯导航通常是由内容区和导航区组成,导航区是相对屏幕固定的,即fixed布局。

页面结构代码如下

<div id="elavatorContainer">
    <div
      v-for="item in list"
      :key="item.id"
      :data-key="item.id"
      :class="['elavatorItem']"
    >
      <p>{{ item.value }}</p>
    </div>
    <div id="navigation">
      <p
        v-for="item in list"
        :key="item.id"
        class="navigationItem"
        @click="scrollTo(item.id)"
      >
        {{ item.value }}
      </p>
    </div>
  </div>

CSS代码如下

#elavatorContainer {
  height: 100vh;
  overflow: auto;
  background: #eee;
  .elavatorItem {
    height: 800px;
    margin: 20px 80px 20px 20px;
    overflow: auto;
    background: #cfcfcf;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  #navigation {
    position: fixed;
    right: 25px;
    top: 50%;
    transform: translateY(-50%);
    background: #d8d8d8;
    padding: 5px;
    border-radius: 10px;
    .navigationItem {
      cursor: pointer;
      transition: all 0.2s ease-in-out;
      border-radius: 5px;
      &:hover {
        background: #fff;
      }
    }
  }
}

这里就是基础的布局方案,不再过多赘述。

手动导航定位

我们需要实现点击对应导航按钮,滚动条自动滚动到对应的楼层,就像是坐电梯按对应楼层的场景~

function scrollTo(id) {
  const el = document.querySelector(`.elavatorItem:nth-child(${id})`);
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

4行代码搞定,看起来是是不是很简单?我们只需要拿到对应的楼层id,然后通过id可以获取到对应楼层的元素引用,然后通过el.scrollIntoView方法就可以自动定位到对应楼层了。

behavior:滚动类型(smooth平滑滚动)

block:定位位置(center居中展示)

这是我们手动定位的操作场景,那如果是直接操作滚动条呢?我们如何知道当前楼层对应的按钮索引呢?(怎么实现楼层按钮高亮)

自动感应楼层变化

一般对于感知dom变化,我们最好可以先想想浏览器原生的一些API(MutationObserver和IntersectionObserver)是否可以实现,我认为这些API的功能还是很强大的,可以少写很多js代码。

既然是感应,那一定有一个感应标识,这里我们可以根据楼层元素和楼元素的交叉情况来判定。因为交叉意味着显隐状态发生了变化,拿到这个关键信息,也就达到我们最初的目的了。

直接上代码

function createInsectionOberser() {
  const observer = new IntersectionObserver(
    (entires) => {
      entires.forEach((entry) => {
        if (entry.isIntersecting) {
          const id = entry.target.dataset.key;
          const navigationItem = document.querySelector(
            `.navigationItem:nth-of-type(${id})`,
          );
          navigationItems.value.map((item) => {
            item.style.background = '#d8d8d8';
          });
          navigationItem.style.background = '#fff';
        }
      });
    },
    {
      threshold: 1,
      root: document.querySelector('#elavatorContainer'),
    },
  );
  const items = Array.from(document.querySelectorAll('.elavatorItem'));
  items.map((item) => observer.observe(item));
}

我们获取所有的楼层元素,并为每一个楼层元素都添加上一个观察器。当楼层和父元素发生交叉的时候,取出当前楼层属性上绑定的key作为楼层标识,注意这里的key要和电梯按钮列表上绑定的key要一致,因为我们要通过楼层的key找到该层对应的电梯按钮的key。

在修改电梯按钮高亮样式前先全部初始化之前的样式样式,保证每一次的更新只有一个楼层按钮被高亮。

做完这些后,电梯导航也就实现完成了,最后别忘了在组件加载完毕时将观察方法注册上去哦~

onMounted(() => {
  navigationItems.value = Array.from(
    document.querySelectorAll('.navigationItem'),
  );
  createInsectionOberser();
});

好了,这就是电梯导航的全部内容,是不是很简单?只要能灵活运用浏览器的观察器API,可以实现很多看起来很复杂的前端场景(其实一点也不复杂)

附上完整代码

<template>
  <div id="elavatorContainer">
    <div
      v-for="item in list"
      :key="item.id"
      :data-key="item.id"
      :class="['elavatorItem']"
    >
      <p>{{ item.value }}</p>
    </div>
    <div id="navigation">
      <p
        v-for="item in list"
        :key="item.id"
        class="navigationItem"
        @click="scrollTo(item.id)"
      >
        {{ item.value }}
      </p>
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue';
const list = ref(
  Array.from({ length: 10 }, (_, i) => ({
    id: i + 1,
    value: `第${i + 1}层`,
  })),
);
const navigationItems = ref([]);
function scrollTo(id) {
  const el = document.querySelector(`.elavatorItem:nth-child(${id})`);
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function createInsectionOberser() {
  const observer = new IntersectionObserver(
    (entires) => {
      entires.forEach((entry) => {
        if (entry.isIntersecting) {
          const id = entry.target.dataset.key;
          const navigationItem = document.querySelector(`.navigationItem:nth-of-type(${id})`);
          navigationItems.value.map((item) => {
            item.style.background = '#d8d8d8';
          });
          navigationItem.style.background = '#fff';
        }
      });
    },
    {
      threshold: 1,
      root: document.querySelector('#elavatorContainer'),
    },
  );
  const items = Array.from(document.querySelectorAll('.elavatorItem'));
  items.map((item) => observer.observe(item));
}
onMounted(() => {
  navigationItems.value = Array.from(
    document.querySelectorAll('.navigationItem'),
  );
  createInsectionOberser();
});
</script>

<style lang="scss" scoped>
#elavatorContainer {
  height: 100vh;
  overflow: auto;
  background: #eee;
  .elavatorItem {
    height: 800px;
    margin: 20px 80px 20px 20px;
    overflow: auto;
    background: #cfcfcf;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  #navigation {
    position: fixed;
    right: 25px;
    top: 50%;
    transform: translateY(-50%);
    background: #d8d8d8;
    padding: 5px;
    border-radius: 10px;
    .navigationItem {
      cursor: pointer;
      transition: all 0.2s ease-in-out;
      border-radius: 5px;
      &:hover {
        background: #fff;
      }
    }
  }
}
</style>

就用了99行代码,非标题党哈🤣