锚点双向定位滚动功能-最终版

核心滑动组件代码: 

<template>
  <div ref="sliderBody" class="slider-main">
    <div class="slider-body">
      <div ref="sliderMenu" class="slider-menu" :style="{transform: 'translateY(' + (top) + 'px)'}">
        <div ref='menuItems' class="slider-menu-item" v-for="(item, index) in data" :key='index' :style="item.style || {}"
          :class="{'slider-menu-item-active': current === index}" @click="scrollToView(index)">
          <span>{{item.name}}</span>
        </div>
      </div>
      <div ref="sliderInfo" class="slider-info">
        <div ref="scrollItem" class="component-info" v-for="(item, index) in data" :key="index + '-cmp'"
          :style="{'min-height': ((index === data.length - 1) ? 0 : 0) + 'px'}">
          <template v-if="item.component">
            <component :is="item.component" :data='item.data'></component>
          </template>
          <template v-else>&nbsp;</template>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'SliderMenu',
  props: {
    data: {
      type: [String, Number, Boolean, Object, Array],
      default: () => []
    },
    scrollHeight: {
      type: [String, Number],
      default: 0
    }
  },
  data () {
    return {
      current: 0,
      bodyOffset: 100,
      topOffset: 1000,
      oneMenuHeight: 39,
      minViewMenuCount: 3,
      windowViewHeight: this.getWindowHeight(),
      menuHeight: 0,
      otherNode: null,
      tmpLocation: 0
    }
  },
  computed: {
    top () {
      if (this.topOffset >= this.bodyOffset) {
        return this.bodyOffset
      }
      return this.topOffset
    },
    currentSliderHeight () { // 当前滑动的菜单高度
      return (this.current + 1) * this.oneMenuHeight
    },
    minViewMenuHeight () { // 菜单最小可见高度
      return this.minViewMenuCount * this.oneMenuHeight
    },
    viewHeight () { // 可视图中,最大能显示的可见高度
      let height = this.windowViewHeight - this.bodyOffset + this.scrollHeight
      return height > 0 ? height : 0
    }
  },
  watch: {
    data () {
      this.init()
    }
  },
  methods: {
    removeNode () {
      if (this.otherNode) {
        document.body.removeChild(this.otherNode)
      }
    },
    createNode () {
      const elink = document.createElement('div')
      elink.style.height = this.viewHeight + 'px'
      elink.style.width = '100%'
      document.body.appendChild(elink)
      this.otherNode = elink
    },
    scrollToView (index) { // 滑动到指定组件位置
      this.windowViewHeight = this.getWindowHeight() // 窗口大小
      const scrollTop = this.getScroll(window, true)
      if (!this.data || !this.data.length) {
        return
      }
      let totalHeight = document.documentElement.scrollHeight
      let hideHeight = this.scrollHeight > scrollTop ? scrollTop : this.scrollHeight
      let baseStartTop = this.bodyOffset - hideHeight // 起始位置 - 菜单向上滚动多少:代表菜单的顶部
      let baseTop = scrollTop + baseStartTop // 滑动高度 + 菜单顶部
      let data = this.getScrollItems()
      let menuTop = this.getInfoTop() - baseTop
      if (index >= 0 && index < data.length) {
        if ((totalHeight - scrollTop <= this.windowViewHeight && this.current < index) ||
          (menuTop >= 0 && this.current >= index)) {
          this.current = index
        } else {
          let top = this.getNodeTop(data[index]) - this.bodyOffset + hideHeight
          window.scrollTo({top: top < 0 ? 0 : top + this.scrollHeight - hideHeight, behavior: 'smooth'})
        }
      }
    },
    handleScroll1 () {
      this.windowViewHeight = this.getWindowHeight()
      if (!this.data || !this.data.length) {
        return
      }
      let data = this.getScrollItems()
      const scrollTop = this.getScroll(window, true)
      let hideHeight = this.scrollHeight > scrollTop ? scrollTop : this.scrollHeight
      let baseTop = scrollTop + this.bodyOffset // 滑动高度 + 起始位置
      let maxTop = -99999
      let maxVisibleBottom = this.getMenuTop() + this.currentSliderHeight + this.minViewMenuHeight // 当前最大能显示的菜单所在位置
      // 小于最小显示数量 || 菜单完全显示在可视视图中 || 当前选中的菜单 + 最小展示的菜单 在视图中
      if (this.current < this.minViewMenuCount || this.menuHeight + this.bodyOffset - hideHeight < this.windowViewHeight) {
        this.topOffset = this.bodyOffset - hideHeight
      }
      let oldCurrent = this.current
      data.forEach((target, index) => {
        if (target) {
          let realTop = this.getNodeTop(target) - baseTop + hideHeight
          if (realTop <= 0 && maxTop <= realTop) {
            maxTop = realTop
            this.current = index
          }
        }
      })
      let minMenuOffset = this.menuHeight - this.viewHeight
      let minTopOffset = this.bodyOffset - hideHeight - (minMenuOffset > 0 ? minMenuOffset : 0) // 菜单完全显示到底时,最大偏移量
      let bottom = this.getInfoBottom() - baseTop - this.viewHeight + this.scrollHeight // 内容显示到底时:bottom=0
      if (this.current >= data.length - 1 && bottom < 0) {
        let lastNodeHeight = (this.getOffset(data[this.current]) || {}).height || 0
        let minHeight = Math.min(this.menuHeight, this.viewHeight) // 判断菜单 和可见视图 哪个小
        // 在 minHeight < lastNodeHeight 时:必定能完全显示最后一个组件页面
        // 他们的差值:代表 (组件页面 + 多少)能与菜单对齐,或者与视图窗口对齐
        let showMenuHeight = this.windowViewHeight - this.bodyOffset + hideHeight // 最大能显示菜单高度
        let hasShowTop = showMenuHeight > this.currentSliderHeight + this.minViewMenuHeight // 完全显示当前锁选范围
        if (hasShowTop) {
          this.topOffset = this.bodyOffset - hideHeight
        } else {
          let bottomOffset = minHeight - lastNodeHeight
          this.topOffset = minTopOffset + bottom + (bottomOffset > 0 ? bottomOffset : 0)
        }
      } else if (oldCurrent !== this.current && this.current >= this.minViewMenuCount &&
        this.windowViewHeight < maxVisibleBottom) {
        let moveHeight = (oldCurrent > this.current ? 1 : -1) * this.oneMenuHeight
        let showMenuHeight = this.windowViewHeight - this.bodyOffset + hideHeight // 最大能显示菜单高度
        let hasShowTop = showMenuHeight > this.currentSliderHeight + this.minViewMenuHeight // 完全显示当前锁选范围
        if (moveHeight > 0 && hasShowTop) {
          this.topOffset = this.bodyOffset - hideHeight
        } else {
          this.topOffset = (this.topOffset < minTopOffset ? minTopOffset : this.topOffset) + moveHeight
        }
      }
    },
    handleScroll () {
      this.windowViewHeight = this.getWindowHeight() // 窗口大小
      const scrollTop = this.getScroll(window, true)
      let hideHeight = this.scrollHeight > scrollTop ? scrollTop : this.scrollHeight // 隐藏offset高度
      let baseStartTop = this.bodyOffset - hideHeight // 起始位置 - 菜单向上滚动多少:代表菜单的顶部
      let baseTop = scrollTop + baseStartTop // 滑动高度 + 菜单顶部
      if (!this.data || !this.data.length) {
        return
      }
      // 判断是否需要滑动菜单
      let data = this.getScrollItems()
      let menus = this.getMenuItems()
      let maxTop = -99999
      let oldCurrent = this.current
      data.forEach((target, index) => {
        if (target) {
          let realTop = this.getNodeTop(target) - baseTop
          if (realTop <= 0 && maxTop <= realTop) { // 最接近基准线
            maxTop = realTop
            this.current = index
          }
        }
      })
      if (this.oldCurrent === this.current) {
        return
      }
      let bottom = this.getInfoBottom() - scrollTop - this.windowViewHeight // 内容底部 - 滑动距离 - 最大可视化窗口=0:表示内容到底了
      let isEnd = this.getInfoBottom() - this.getMenuBottom() // =0时:菜单与内容底部齐平
      let maxMenuIndex = this.current + this.minViewMenuCount >= menus.length ? menus.length - 1 : (this.current + this.minViewMenuCount)
      let minMenuIndex = this.current - this.minViewMenuCount < 0 ? 0 : (this.current - this.minViewMenuCount)
      let maxMenuBottom = this.getNodeBottom(menus[maxMenuIndex]) - scrollTop - this.windowViewHeight // 最大可见菜单滑动到底了
      let maxMenuTop = this.getNodeTop(menus[minMenuIndex]) - baseTop // 是否到顶
      let menuTop = this.getMenuTop() - baseTop
      let menuBottom = this.getMenuBottom() - scrollTop - this.windowViewHeight // 菜单到底了
      // if (menuBottom < 0 && bottom > 0) { console.info('菜单到底,内容未到底')
      // this.topOffset += isEnd
      // 否则:菜单未到底,或者 内容到底
      if (bottom < 0 && isEnd <= 0) { // 内容到底,并超过菜单,菜单移动超过距离
        this.topOffset += isEnd
      } else if (this.current < this.minViewMenuCount || baseStartTop + this.menuHeight < this.windowViewHeight ||
        this.getMenuTop() > baseTop) {
        // 当前位置小于最小可见,或者 整个菜单在可视范围内,或者 菜单高度 低于起始线。
        this.topOffset = baseStartTop
      } else if (oldCurrent > this.current && this.topOffset < baseTop && maxMenuTop <= 0 && menuTop < 0) {
        // 向上滑动:未到达菜单顶部,最大可见菜单到顶
        this.topOffset += this.oneMenuHeight
      } else if (oldCurrent < this.current && maxMenuBottom >= 0 && menuBottom > 0) {
        // 向下滑动 :最大可见菜单到底,全部菜单未到底
        this.topOffset -= this.oneMenuHeight
      } else if (isEnd >= 0 && menuBottom < 0 && maxMenuTop < 0) {
        // 最大可见菜单到顶,菜单底部到底
        // this.topOffset += isEnd
      }
    },
    getWindowHeight () {
      return window.innerHeight || document.documentElement.clientHeight
    },
    getNodeBottom (node) {
      const currentOffset = this.getOffset(node) || {}
      return (currentOffset.top || 0) + (currentOffset.height || 0)
    },
    getNodeTop (node) {
      const currentOffset = this.getOffset(node) || {}
      return (currentOffset.top || 0)
    },
    getMenuTop () {
      if (this.$refs.sliderMenu) {
        return this.getNodeTop(this.$refs.sliderMenu)
      }
      return 0
    },
    getMenuBottom () {
      if (this.$refs.sliderMenu) {
        return this.getNodeBottom(this.$refs.sliderMenu)
      }
      return 0
    },
    getBodyTop () {
      if (this.$refs.sliderBody) {
        return this.getNodeTop(this.$refs.sliderBody)
      }
      return 0
    },
    getInfoTop () {
      if (this.$refs.sliderInfo) {
        return this.getNodeTop(this.$refs.sliderInfo)
      }
      return 0
    },
    getInfoBottom () {
      if (this.$refs.sliderInfo) {
        return this.getNodeBottom(this.$refs.sliderInfo)
      }
      return 0
    },
    getScrollItems () {
      let refs = []
      if (this.$refs.scrollItem && this.$refs.scrollItem.style) {
        refs.push(this.$refs.scrollItem)
      } else if (this.$refs.scrollItem && this.$refs.scrollItem.length) {
        refs = this.$refs.scrollItem
      }
      return refs
    },
    getMenuItems () {
      let refs = []
      if (this.$refs.menuItems && this.$refs.menuItems.style) {
        refs.push(this.$refs.menuItems)
      } else if (this.$refs.menuItems && this.$refs.menuItems.length) {
        refs = this.$refs.menuItems
      }
      return refs
    },
    getScroll (target, top) {
      const prop = top ? 'pageYOffset' : 'pageXOffset'
      const method = top ? 'scrollTop' : 'scrollLeft'
      let ret = target[prop]
      if (typeof ret !== 'number') {
        ret = window.document.documentElement[method]
      }
      return ret
    },
    getOffset (element) {
      if (!element) {
        return {top: 0, height: 0, left: 0, width: 0}
      }
      const rect = element.getBoundingClientRect()
      const scrollTop = this.getScroll(window, true)
      const scrollLeft = this.getScroll(window)

      const docEl = window.document.body
      const clientTop = docEl.clientTop || 0
      const clientLeft = docEl.clientLeft || 0
      let height = rect.bottom - rect.top
      if (height === 0 && element.parentNode) {
        let parentRect = element.parentNode.getBoundingClientRect()
        height = parentRect ? parentRect.height || 0 : 0
      }
      return {
        top: rect.top + scrollTop - clientTop,
        left: rect.left + scrollLeft - clientLeft,
        height: height,
        width: rect.right - rect.left
      }
    },
    init () {
      this.$nextTick(() => {
        this.initMenuHeight()
        this.scrollToView(0)
        this.handleScroll()
      })
    },
    initMenuHeight () {
      this.menuHeight = (this.getOffset(this.$refs.sliderMenu) || {}).height || 0
    }
  },
  mounted () {
    this.bodyOffset = this.getNodeTop(this.$refs.sliderBody)
    this.topOffset = this.bodyOffset
    window.addEventListener('scroll', this.handleScroll, false)
    window.addEventListener('resize', this.handleScroll, false)
    this.init()
  },
  beforeDestroy () {
    window.removeEventListener('scroll', this.handleScroll, false)
    window.removeEventListener('resize', this.handleScroll, false)
    // this.removeNode()
  }
}
</script>
<style scoped>
.slider-main {
  display: flex;
  flex-direction: column;
}
.slider-body {
  display: flex;
  flex-wrap: wrap;
}
.slider-menu {
  position: fixed;
  left: auto;
  width: 180px;
  overflow: auto;
  color: #000;
  height: auto;
  top: 0px;
  transition:transform 0.1s linear;
}
.slider-menu-item {
  border-left: 2px solid #e8eaed;
  display: block;
  padding: 15px 0 0 30px;
  position: relative;
  line-height: 24px;
  color: inherit;
  cursor: pointer;
}
.slider-menu-item-active, .slider-menu-item:hover {
  color: #ff5000;
}
.slider-menu-item-active {
  border-left: 2px solid #ff5000;
}
.component-info {
  min-height: 50px;
  width: 100%;
}
.slider-info {
  margin-left: 200px;
  display: flex;
  flex-direction: column;
  position: relative;
  width: 100%;
}
.menu-move-animation {
  width: 100%;
  padding: 0px;
  animation-duration: 0.3s;
  animation-fill-mode: both;
  animation-name: fadeInLeft;
}
@keyframes fadeInLeft {
  from {
    opacity: 0;
    /* transform: translate3d(100%, 0, 0); */
  }
  to {
    opacity: 1;
    transform: none;
  }
}
</style>

应用例子

<template>
  <div v-if="show" style="position:relative;">
    <SliderMenu :scrollHeight='scrollHeight' :data='tagData'></SliderMenu>
  </div>
</template>
<script>
import SliderMenu from '@/components/platform/common/slider-menu'
import TagCard from '@/components/platform/common/tag-info/common/tag-card'
import TagNumCard from '@/components/platform/common/tag-info/common/tag-num-card'
import TagTypeCard from '@/components/platform/common/tag-info/common/tag-type-card'
import SliderTitle from '@/components/platform/common/base-card/slider-title'
export default {
  name: 'PhotoTagInfo',
  components: { SliderMenu},
  props: {
    show: {
      type: [String, Number, Boolean],
      default: true
    },
    scrollHeight: {
      type: [String, Number],
      default: 0
    }
  },
  data () {
    return {
      id: null,
      treeLoading: false,
      loading: false,
      intrestTree: [],
      intrestData: [],
      ipData: [],
      otherData: [],
      treeData: [],
      tagTip: []
    }
  },
  created () {
    this.loadMenuTipConfig()
    this.loadTagTree()
  },
  computed: {
    tagData () {
      let tipMap = {}
      let tipData = this.tagTip || []
      tipData.forEach(item => { tipMap[item.name] = item })
      let tmpData = [
        {name: '河图', style: {color: '#202122', 'font-size': '16px', 'font-weight': '600'}, component: SliderTitle, data: {title: '河图'}}
      ]
      let tmp = {}
      tmp.title = '兴趣类目'
      tmp.data = this.intrestTree
      tmp.tip = (tipMap[tmp.title] || {}).tip
      tmp.link = this.getLinkData(tipMap[tmp.title])
      tmpData.push({name: tmp.title, style: {}, component: TagCard, data: tmp})
      tmp = {}
      tmp.title = 'IP标签'
      tmp.data = this.ipData
      tmp.tip = (tipMap[tmp.title] || {}).tip
      tmp.link = this.getLinkData(tipMap[tmp.title])
      tmpData.push({name: tmp.title, style: {}, component: TagTypeCard, data: tmp})
      tmp = {}
      tmp.title = '细分标签'
      tmp.data = this.otherData
      tmp.tip = (tipMap[tmp.title] || {}).tip
      tmp.link = this.getLinkData(tipMap[tmp.title])
      tmpData.push({name: tmp.title, style: {}, component: TagNumCard, data: tmp})
      return tmpData
    }
  },
  methods: {
    getLinkData (data) {
      let tmp = data || {}
      return [
        {name: '相关视频', link: tmp.photoLink},
        {name: '相关详情', link: tmp.docLink || tmp.link}
      ]
    }
  }
}
</script>

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值