效果
使用方法:
1. 组件引入,进行绑定
// 右侧锚点
<BAnchor class="tab-nav" ref="anchor" container=".content-info" // 绑定监听的容器 target=".main-box" // 绑定滚动的容器 :targetOffset="50" ></BAnchor>
2.左侧标题
<div class="form-title"> <h3 class="c-title">基本信息</h3> <el-divider /> <span></span> </div>
/*js*/const anchor = ref()onMounted(async () => { nextTick(() => { anchor.value?.updateNav() })})
组件
<template> <div class="b-anchor"> <i class="toc-line"></i> <nav class="toc-content"> <span class="toc-content-heading" v-if="title">{{ title }}</span> <ul class="toc-items"> <li v-for="(v, k) in navs" :key="k" :class="[{ active: active == k }, d1((v as HTMLDivElement).nodeName)]" @click="scrollTo(k as number)" > {{ (v as HTMLDivElement).innerText }} </li> </ul> </nav> </div></template><script setup lang="ts" name="BAnchor">import { debounce } from 'lodash'import { onMounted, onUnmounted, ref } from 'vue'interface Props { // 指定监听的容器 container: string // 滚动容器 target?: string // 标题 title?: string // 距离窗口顶部达到指定偏移量 targetOffset?: number}const props = withDefaults(defineProps<Props>(), { targetOffset: 0})const active = ref(0)const navs = ref<any>({})const target = ref({} as Element | Window)const d1 = (val: string) => { switch (val) { case 'H1': case 'H2': return 'd2' case 'H3': return 'd3' default: return 'd4' }}// 滚动监听器const onScroll = debounce(() => { // 所有锚点元素的 offsetTop console.log('v', 22) const offsetTopArr: number[] = [] navs.value.forEach((v: any) => { offsetTopArr.push(v.offsetTop) }) const scroll = target.value instanceof Element ? target.value.scrollTop : undefined // 获取当前文档流的 scrollTop const scrollTop = scroll || document.documentElement.scrollTop || document.body.scrollTop // 定义当前点亮的导航下标 offsetTopArr.forEach((v, k) => { if (scrollTop >= v - 10 - props.targetOffset) { active.value = k } })}, 250)// 跳转到指定索引的元素const scrollTo = (k: number) => { const tar = navs.value.item(k) if (props.target) { target.value.scrollTo({ top: tar.offsetTop - props.targetOffset, behavior: 'smooth' }) } else { document.documentElement.scrollTo({ top: tar.offsetTop - props.targetOffset, behavior: 'smooth' }) } console.log('scrollTo', k, navs.value, tar.offsetTop)}const updateNav = () => { // 获取所有锚点元素 navs.value = document .querySelector(props.container) ?.querySelectorAll('h1,h2,h3,h4,h5,h6')}onMounted(() => { if (props.target) { target.value = document.querySelector(props.target) as Element } else { target.value = window } // 获取所有锚点元素 navs.value = document .querySelector(props.container) ?.querySelectorAll('h1, h2, h3, h4, h5, h6') target.value.addEventListener('scroll', onScroll)})onUnmounted(() => { target.value.removeEventListener('scroll', onScroll)})defineExpose({ updateNav })</script><style lang="scss" scoped>@mixin ellipsis { text-overflow: ellipsis; overflow: hidden; white-space: nowrap;}.b-anchor { position: fixed; width: inherit; height: inherit; overflow-y: auto; overflow-x: hidden;}.toc-line { position: absolute; top: 0; left: 0; bottom: 0; width: 1px; background: #dfe1e8; z-index: -1;}.toc-content { .toc-content-heading { font-size: 12px; font-weight: 600; text-transform: uppercase; margin: 0; } .toc-items { list-style: none; padding: 0; li { position: relative; font-size: 14px; line-height: 28px; color: #5e6373; font-weight: 400; text-indent: 20px; @include ellipsis; cursor: pointer; + li { margin-top: 8px; } } li:hover { background: #f7f8fa; border-radius: 4px; color: #409eff; // border-left: 1px solid #dfe1e8; } .active { color: #409eff; } .d2 { font-weight: 600; } .d3 { padding-left: 15px; } .d4 { padding-left: 35px; } .active::before { content: ''; position: absolute; left: 0; background-color: #409eff; border-radius: 1px; width: 2px; height: 28px; top: 0px; } }}</style>