目录红色节点随内容滚动,可锚点到内容

815 阅读1分钟

实现内容

网页分为上下两块,上块包含左侧目录右侧内容,下块为底部页脚,右侧内容滚动,左侧目录吸顶。当露出页脚时,目录应该随着内容滚动高度逐渐变小,目录当前定位的红色圆点,也随之展示。点击目录时,内容可锚点展示。

布局如下

初始状态

image.png

未滚动到底部 image.png

滚动到底部 image.png

时间轴组件

代码如下

<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开始),与htmlid配合使用。
nextTick(() => {
        // 用于当前选中的节点能展示出来
        const ele = document.getElementById(`verticalTimeline${currentValue.value}`)
        ele.scrollIntoView(true)
    })