锚点双向定位滚动功能-升级版(vue)
对上一文章中的功能进行进一步的优化升级:锚点双向定位滚动功能(Vue实现)效果1(窗口大于菜单):效果2(窗口小于菜单):效果3(点击事件):代码实现:<template><div ref="sliderBody" class="slider-main"><div class="slider-body"><div ref="sliderMenu" clas
·
对上一文章中的功能进行进一步的优化升级:锚点双向定位滚动功能(Vue实现)
效果1(窗口大于菜单):
效果2(窗口小于菜单):
效果3(点击事件):
代码实现:
<template>
<div ref="sliderBody" class="slider-main">
<div class="slider-body">
<div ref="sliderMenu" class="slider-menu" :style="{transform: 'translateY(' + top + 'px)'}">
<div 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) ? menuHeight : 0) + 'px'}">
{{index}}
<component :is="item.component"></component>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SliderMenu',
props: {
data: {
type: [String, Number, Boolean, Object, Array],
default: () => [
{name: '技术能力', style: {}, component: {}},
{name: '语音技术', style: {}, component: {}},
{name: '文字识别', style: {}, component: {}},
{name: '人脸与人体识别', style: {}, component: {}},
{name: '视频技术', style: {}, component: {}},
{name: 'AR与VR', style: {}, component: {}},
{name: '数据智能', style: {}, component: {}},
{name: 'AR与VR', style: {}, component: {}},
{name: '数据智能', style: {}, component: {}},
{name: 'AR与VR', style: {}, component: {}},
{name: '数据智能', style: {}, component: {}},
{name: 'AR与VR', style: {}, component: {}},
{name: '数据智能', style: {}, component: {}}
]
}
},
data () {
return {
current: 0,
bodyOffset: 10,
topOffset: 10,
oneMenuHeight: 39,
minViewMenuCount: 3,
windowViewHeight: this.getWindowHeight(),
menuHeight: 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 () { // 可视图中,最大能显示的可见高度
return this.windowViewHeight - this.bodyOffset
}
},
methods: {
scrollToView (index) { // 滑动到指定组件位置
let data = this.getScrollItems()
if (index >= 0 && index < data.length) {
let top = this.getNodeTop(data[index]) - this.bodyOffset
window.scrollTo({top: top < 0 ? 0 : top, behavior: 'smooth'})
}
},
handleScroll () {
let data = this.getScrollItems()
this.windowViewHeight = this.getWindowHeight()
const scrollTop = this.getScroll(window, true)
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 < this.windowViewHeight
|| maxVisibleBottom < this.windowViewHeight) {
this.topOffset = this.bodyOffset
}
let oldCurrent = this.current
data.forEach((target, index) => {
const elOffset = this.getOffset(target)
let realTop = this.getNodeTop(target) - baseTop
if (realTop <= 0 && maxTop <= realTop) {
maxTop = realTop
this.current = index
}
})
let minMenuOffset = this.menuHeight - this.viewHeight
let minTopOffset = this.bodyOffset - minMenuOffset // 菜单完全显示到底时,最大偏移量
let bottom = this.getInfoBottom() - baseTop - this.viewHeight // 内容显示到底时: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 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
this.topOffset = (this.topOffset < minTopOffset ? minTopOffset : this.topOffset) + moveHeight
}
},
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
},
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
},
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) {
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
}
}
},
mounted () {
this.bodyOffset = this.$refs.sliderBody.offsetTop
this.menuHeight = (this.getOffset(this.$refs.sliderMenu) || {}).height || 0
this.topOffset = this.bodyOffset
window.addEventListener('scroll', this.handleScroll, false)
window.addEventListener('resize', this.handleScroll, false)
this.handleScroll()
},
beforeDestroy () {
window.removeEventListener('scroll', this.handleScroll, false)
window.removeEventListener('resize', this.handleScroll, false)
}
}
</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: #1a73e8;
}
.slider-menu-item-active {
border-left: 2px solid #1a73e8;
}
.component-info {
height: 100px;
width: 600px;
border: 1px solid #f0f0f0;
}
.slider-info {
margin-left: 200px;
display: flex;
flex-direction: column;
position: relative;
}
.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>
更多推荐
已为社区贡献2条内容
所有评论(0)