实现内容
网页分为上下两块,上块包含左侧目录右侧内容,下块为底部页脚,右侧内容滚动,左侧目录吸顶。当露出页脚时,目录应该随着内容滚动高度逐渐变小,目录当前定位的红色圆点,也随之展示。点击目录时,内容可锚点展示。
布局如下
初始状态
未滚动到底部
滚动到底部
时间轴组件
代码如下
<template>
<div :class="getCustomClassName" ref="verticalTimeRef" :style="`--dynamicHeight: ${dynamicHeight}px`">
<div
v-for="(item, index) in sourceData"
:key="`verticalTimeline-${index}`"
ref="verticalTimelineRef"
:id="`verticalTimeline${index}`"
:class="getTimelineClassName(index)"
@click="handleClick(index)"
>
<div class="vertical-timeline__item-line"></div>
<div class="vertical-timeline__item-node"></div>
<div class="vertical-timeline__item-wrapper">{{ item.settledName }}</div>
</div>
</div>
</template>
<script lang="ts">
// 竖向时间轴
export default {
name: 'VerticalTimeline',
}
</script>
<script setup lang="ts">
import { computed, ref, watch, onMounted, nextTick } from 'vue'
import _ from 'lodash'
import { usePageScorllStore } from '@/stores/userStore'
import { ID_CONFIG } from '@/common/constants'
const props = defineProps({
sourceData: {
type: Array<any>,
default: () => [],
},
scrollbarRefs: {
type: Array<any>,
default: () => [],
},
})
const emits = defineEmits<{
(event: 'onSelect', data: number): void
}>()
// 页面滚动 Store
const usePageScorll = usePageScorllStore()
const currentValue = ref(0) // 当前选中的值
const verticalTimelineRef = ref(null)
const verticalTimeRef = ref(null)
const conentHeight = ref(0) // 内容高度
const fixedHeight = 96 // 内容下边距 72 内容下内边距24
const openStoreBtnHeight = document.getElementById(ID_CONFIG.OPENING_STORE_ID)?.offsetHeight ?? 98 // 【底部按钮】
const pageFooterHeight = document.getElementById(ID_CONFIG.PAGE_FOOTER_ID)?.offsetHeight ?? 477 // 页脚
const navigatorHeight = document.getElementById(ID_CONFIG.NAVIGATOR_ID)?.offsetHeight ?? 56 // 导航
const scrollbarFixedHeight = navigatorHeight + 40 + 4 // 40px 为内容区域设置的scroll-margin属性值 4px是为了节点选中的时机更准确
// 底部总高度 = 页脚高度 + 底部按钮高度 + 页面底部边距
const bottomTotalHeight = pageFooterHeight + openStoreBtnHeight + fixedHeight
const dynamicHeight = ref(0) // 当目录固定时,随着页面滚动,目录的高度需要动态变化
// 更新选择的节点
const updateActiveNode = (pageScorllTop: number) => {
const { scrollbarRefs = [] } = props
if (!scrollbarRefs.length) return
let found = false
for (let i = 0; i < scrollbarRefs.length; i++) {
const node = scrollbarRefs[i]
const nodeTop = node.offsetTop
if (pageScorllTop + scrollbarFixedHeight >= nodeTop) {
currentValue.value = i
found = true
} else if (found) {
break // 一旦找到第一个匹配的节点,就停止循环
}
}
// 更新样式
verticalTimelineRef.value.forEach((node, index) => {
if (index === currentValue.value) {
node.classList.add('vertical-timeline__item-active')
} else {
node.classList.remove('vertical-timeline__item-active')
}
})
}
// 监听滚动,设置时间轴选中状态
watch(
() => usePageScorll.$state.scorllTop,
val => {
updateActiveNode(val)
}
)
// 获取类样式
const getCustomClassName = computed(() => {
let className = 'vertical-timeline'
const { scorllTop, scorllHeight } = usePageScorll.$state
// 滚动超过200时,增加吸顶样式
if (scorllTop > 200) {
className += ' vertical-timeline__ceiling'
// 页面滚动总高度 与 底部bottomTotalHeight 的间距
const scorllSpacingHeight = scorllHeight - bottomTotalHeight
// 页面总高度 - 目录内容原本的高度
const height = scorllSpacingHeight - conentHeight.value
// 页面滚动距离 > height 时,让悬浮块动态调整高度,并将选中的节点漏出
if (scorllTop > height) {
className += ' vertical-timeline__ceiling-max-height'
dynamicHeight.value = scorllSpacingHeight - scorllTop
nextTick(() => {
// 用于当前选中的节点能展示出来
const ele = document.getElementById(`verticalTimeline${currentValue.value}`)
ele.scrollIntoView(true)
})
}
}
return className
})
// 时间轴的类样式(默认第一个div是选中状态)
const getTimelineClassName = (index: number) => {
return index === 0 && usePageScorll.$state.scorllTop === 0
? 'vertical-timeline__item vertical-timeline__item-active'
: 'vertical-timeline__item'
}
// 点击目录项
const handleClick = _.debounce((index: number) => {
emits('onSelect', index)
}, 300)
onMounted(() => {
conentHeight.value = verticalTimeRef.value?.offsetHeight
})
</script>
<style lang="scss" scoped>
@import '@/framework/styles/_mixin';
@import './index.scss';
</style>
.vertical-timeline {
width: 100%;
// 时间轴
.vertical-timeline__item {
display: flex;
position: relative;
padding-left: 26px;
margin-bottom: 20px;
cursor: pointer;
&:last-child {
margin-bottom: 0;
// 最后一个div不需要 竖线
.vertical-timeline__item-line {
display: none;
}
}
// 竖线
&-line {
position: absolute;
top: 23px;
left: 6px;
width: 2px;
height: 22px;
background: var(--color-line-100, rgba(235, 235, 235, 1));
}
// 圆点
&-node {
position: absolute;
left: 3px;
top: 7px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-fill-300, rgba(217, 217, 217, 1));
}
// 内容
&-wrapper {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: var(--color-text-300, rgba(89, 89, 89, 1));
@include moreline-ellipsis(1);
}
}
// 选中状态
.vertical-timeline__item-active {
.vertical-timeline__item-node {
background: var(--color-error-normal, rgba(237, 40, 40, 1));
}
.vertical-timeline__item-wrapper {
font-weight: 500;
}
}
}
// 固定状态
.vertical-timeline__ceiling {
position: fixed;
top: 100px;
width: 340px;
background-color: #fff;
border-radius: 12px;
}
// 固定状态 设置高度 滚动
.vertical-timeline__ceiling-max-height {
height: var(--dynamicHeight);
overflow-y: hidden;
scrollbar-width: none; /* 对于Firefox 隐藏滚动条 */
// 针对Webkit浏览器隐藏滚动条
&::-webkit-scrollbar {
display: none; /* 对于Chrome, Safari和Opera */
}
}
备注
dynamicHeight重要变量,用于高度的变化。多余的用overflow-y: hidden隐藏。- 红色背景需要展示出来,用
nextTick实现,其中currentValue是当前选中的index值(0开始),与html中id配合使用。
nextTick(() => {
// 用于当前选中的节点能展示出来
const ele = document.getElementById(`verticalTimeline${currentValue.value}`)
ele.scrollIntoView(true)
})