Vue实现锚点定位导航功能

项目中路由用的hash模式,不好借助id锚点跳转,只能实现个类似的。

效果图

Anchor.vue

<template>
  <div class="anchor clearfix" ref="anchor">
    <ul class="anchor__nav card">
      <li
        v-for="(item, key) in anchorNavData"
        :key="key"
        :class="{ active: value === key, disabled: item.disabled }"
        @click="arrive(key, item)"
      >
        <i v-if="item.icon" :class="item.icon"></i>
        <span class="ellipsis" :title="item.label">{{ item.label }}</span>
      </li>
    </ul>
    <div class="anchor__content">
      <slot></slot>
    </div>
  </div>
</template>

<script>
import utils from '@/utils'
import { getScrollContainer } from '@/utils/dom'
export default {
  name: 'Anchor',
  props: {
    // 当前楼层
    value: {
      type: String,
      default: ''
    },
    // 滚动的元素,不传默认取离元素最近的可以滚动的元素
    scrollDom: {
      type: String,
      default: ''
    },
    //固定(position: fixed)在滚动区域类的高度
    fixedHeight: {
      type: Number,
      default: 54
    }
  },
  data() {
    return {
      anchorNavData: {}, //电梯楼层高度的映射
      scrollFunDebounceFun: utils.vueDebounce('scrollFun', 100),
      valueType: 1, //当前楼层改变的类型,1是组件传递、按钮直达改变,2是通过滚动监听自动改变
      children: []
    }
  },
  computed: {
    scrollWrap() {
      if (this.scrollDom) {
        return document.querySelector(this.scrollDom)
      } else {
        return getScrollContainer(this.$refs.anchor, true)
      }
    }
  },
  watch: {
    // 监听楼层变化,当为父组件传递,或者按钮直达时,手动滚动到当前楼层
    value(val) {
      if (this.valueType === 1) {
        const scrollTop = this.getAnchorItemInfo(
          this.anchorNavData[val]?.dom
        ).scrollTop
        this.scrollElement(scrollTop)
      } else {
        this.valueType = 1
      }
    }
  },
  mounted() {
    this.calcChildren()
    this.init()
    let scrollTop = 0
    if (this.anchorNavData[this.value]) {
      scrollTop = this.getAnchorItemInfo(
        this.anchorNavData[this.value].dom
      ).scrollTop
      this.scrollElement(scrollTop)
    } else {
      scrollTop = this.getScrollTop(this.scrollWrap)
      this.getAnchorValue(scrollTop)
    }
    this.scrollWrap.addEventListener('scroll', this.scrollFunDebounce)
  },
  beforeDestroy() {
    this.scrollWrap.removeEventListener('scroll', this.scrollFunDebounce)
  },
  methods: {
    calcChildren() {
      let children = this.$slots.default.map((item) => item.componentInstance)
      this.children = treeDeep(children)
      function treeDeep(data, res = []) {
        data.forEach((item) => {
          if (item.$vnode.tag.indexOf('AnchorItem') > -1) {
            res.push(item)
          } else {
            item.$children &&
              item.$children.length &&
              treeDeep(item.$children, res)
          }
        })
        return res
      }
    },
    // 初始化,获取电梯按钮数据
    init() {
      this.children.forEach((item) => {
        this.$set(this.anchorNavData, item.name, {
          dom: item.$el,
          label: item.label,
          icon: item.icon,
          disabled: item.disabled
        })
      })
    },
    /**
     *
     * @param {Dom} e
     * @description 获取元素的scrollTop
     */
    getScrollTop(e) {
      if (e.scrollTop !== undefined) {
        return e.scrollTop
      } else if (e.document) {
        return (
          e.document.documentElement.scrollTop ||
          window.pageYOffset ||
          e.document.body.scrollTop ||
          0
        )
      }
      return e.documentElement.scrollTop || e.body.scrollTop || 0
    },
    // 获取AnchorItem dom 的scrollTop,bottom,height
    getAnchorItemInfo(dom) {
      if (!dom) {
        return {}
      }
      const scrollTop = this.getScrollTop(this.scrollWrap)
      const rect = dom.getBoundingClientRect()
      return {
        scrollTop: rect.top - this.fixedHeight + scrollTop,
        bottom: rect.bottom - this.fixedHeight + scrollTop,
        height: rect.height
      }
    },

    //滚动的处理函数
    scrollFun(e) {
      const scrollTop = this.getScrollTop(e.target)
      this.getAnchorValue(scrollTop)
    },
    // 函数防抖
    scrollFunDebounce(e) {
      this.scrollFunDebounceFun(e)
    },
    // 根据当前高度获取楼层位置,通过计算每个元素在可视区域的高度,与自身实际高度的占比(全部都不在可视区域为0,全部都在则为1),判断当前处在那个楼层
    getAnchorValue(scrollTop) {
      let obj = {}
      for (let i = 0; i < this.children.length; i++) {
        const item = this.getAnchorItemInfo(this.children[i].$el)
        const scrollWrapHeight = this.scrollWrap.getBoundingClientRect
          ? this.scrollWrap.getBoundingClientRect().height
          : this.scrollWrap.innerHeight
        const height = scrollWrapHeight + scrollTop - this.fixedHeight
        let value = 0
        if (scrollTop <= item.scrollTop) {
          value = height - item.scrollTop
          value = value > item.height ? item.height : value < 0 ? 0 : value
        } else if (scrollTop > item.scrollTop && scrollTop <= item.bottom) {
          value = item.bottom - scrollTop
        }
        value = value / item.height
        if (obj[value] === undefined) {
          obj[value] = [this.children[i].name]
        } else {
          obj[value].push(this.children[i].name)
        }
      }
      const maxKey = Math.max(...Object.keys(obj))
      this.valueType = 2
      // 如果占比相同,则取第一个 后期可以修改下 向下滚动取第一个,向上滚动取最后一个
      this.$emit('input', obj[maxKey][0])
    },
    //直达楼层
    arrive(name, item) {
      if (name === this.value || item.disabled) {
        return
      }
      this.valueType = 1
      this.$emit('input', name)
    },
    // 模拟滚动
    scrollElement(itemScrollTop) {
      const scrollTop = this.getScrollTop(this.scrollWrap)
      this.scrollAnimation(this.scrollWrap, scrollTop, itemScrollTop)
    },
    /**
     *
     * @param {Dom} el 要滚动的元素
     * @param {Number} start 滚动元素的初始位置
     * @param {Number} end 滚动元素的初始位置
     * @description 滚动动画
     */
    scrollAnimation(el, start, end) {
      let step = 0
      // 根据距离计算步长,表现在先快后慢
      const interval = Math.abs(end - start)
      if (interval > 1000) {
        step = 100
      } else if (interval > 500) {
        step = 50
      } else if (interval > 200) {
        step = 30
      } else {
        step = 20
      }
      if (start < end) {
        start = start + step
        if (start > end) {
          start = end
        }
      }
      if (start > end) {
        start = start - step
        if (start < end) {
          start = end
        }
      }
      el.scrollTo(0, start)
      if (start !== end) {
        requestAnimationFrame(() => {
          this.scrollAnimation(el, start, end)
        })
      }
    }
  }
}
</script>

<style lang="scss">
.anchor__nav.card {
  position: sticky;
  top: $headerHeight;
  z-index: 10;
  float: left;
  width: 200px;
  padding: 15px 20px;

  li {
    line-height: 20px;
    height: 20px;
    width: fit-content;
    user-select: none;
    margin: 20px 0;
    cursor: pointer;
    &:hover {
      color: $mainColor;
    }
    &.active {
      color: $mainColor;
    }
    &.disabled {
      color: #ccc;
      cursor: not-allowed;
    }
    i {
      margin-right: 6px;
      vertical-align: top;
    }
    span {
      display: inline-block;
      max-width: 140px;
    }
  }
}
.anchor__content {
  float: right;
  width: calc(100% - 220px);
}
</style>

AnchorItem.vue

<template>
  <div class="anchor-item card" :class="{ disabled }">
    <slot name="title">
      <div class="anchor-item__title">
        {{ label }}
      </div>
    </slot>
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'AnchorItem',
  props: {
    // 楼层标题
    label: {
      type: String,
      default: ''
    },
    // 楼层名
    name: {
      type: String,
      required: true
    },
    icon: {
      type: String,
      default: ''
    },
    disabled: {
      type: Boolean,
      default: false
    }
  }
}
</script>

<style lang="scss">
.anchor-item.card {
  padding: 0;
  margin-bottom: 20px;
  border-radius: 4px;
  overflow: visible;
}
.anchor-item__title {
  height: 60px;
  line-height: 60px;
  font-size: 16px;
  padding: 0 20px;
  border-bottom: 1px solid #f5f5f5;
  background: $whiteColor;
  &.sticky {
    position: sticky;
    top: $headerHeight;
	z-index: 9;
  }
}
</style>

在页面中使用

<template>
    <div class="container">
        <anchor v-model="anchor">
            <anchor-item label="文章一" name="1" class="anchor-item">
                <div class="text">
                    <cy-text
                        value="Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。"
                    ></cy-text>
                </div>
            </anchor-item>
            <anchor-item label="文章二" name="2" class="anchor-item" style="height: 400px;">
                <div class="text">
                    <cy-text
                        value="Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。"
                        row="2"
                    ></cy-text>
                </div>
            </anchor-item>
            <anchor-item label="文章三" name="3" class="anchor-item" style="height: 500px;">
                <div class="text">
                    <cy-text
                        value="Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。"
                        row="3"
                    ></cy-text>
                </div>
            </anchor-item>
            <anchor-item label="文章四" name="4" class="anchor-item" style="height: 600px;">
                <div class="text">
                    <cy-text
                        value="Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。"
                        row="4"
                    ></cy-text>
                </div>
            </anchor-item>
        </anchor>
    </div>
</template>

<script>
export default {
    name: 'Anchor',
    data() {
        return {
            anchor: '1'
        }
    },
}
</script>

<style scoped lang="scss">
.anchor-item {
    margin-bottom: 15px;
    background-color: #fff;
    height: 300px;

    .text {
        padding: 20px;
        width: 600px;
    }
}
</style>

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值