68 lines
2.1 KiB
Vue
68 lines
2.1 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* TOC 条目(递归)—— 照抄 Docus / @nuxt/ui ContentToc 的 ul>li>a 结构。
|
||
*
|
||
* 实测(docus.dev,1440px / light):
|
||
* ul.list / ul.listWithChildren
|
||
* a.link display:flex; align-items:center; padding:4px 0;
|
||
* font-size:14px; line-height:20px; height:28px
|
||
* border-radius:4px(rounded-sm)
|
||
* color: text-muted
|
||
* a.link:active color: text-primary;font-weight 仍为 400
|
||
* h3 层级由父级 ul 的缩进体现(ms-3 = 12px)
|
||
*/
|
||
import { computed } from 'vue'
|
||
|
||
interface TocLink {
|
||
id: string
|
||
text: string
|
||
children?: TocLink[]
|
||
}
|
||
|
||
const props = defineProps<{
|
||
link: TocLink
|
||
level: number
|
||
activeIds: string[]
|
||
}>()
|
||
|
||
/** 点击时通知父级锁定该项为激活,避免滚动动画期间被 scrollspy 抢走 */
|
||
const emit = defineEmits<{ navigate: [id: string] }>()
|
||
|
||
const isActive = computed(() => props.activeIds.includes(props.link.id))
|
||
|
||
const linkClass = computed(() => ['g3-toc-link', isActive.value ? 'is-active' : ''])
|
||
|
||
function onClick(e: MouseEvent) {
|
||
const el = document.getElementById(props.link.id)
|
||
if (!el) return
|
||
e.preventDefault()
|
||
// 先锁定,再滚动:这样无论落点是多少,点击的项一定是高亮的
|
||
emit('navigate', props.link.id)
|
||
el.focus({ preventScroll: true })
|
||
// 滚到与 scrollspy 判据一致的位置:标题顶部对齐视口顶部下方一点,
|
||
// 使「绝对位置 <= scrollY + 余量」在滚动结束后必然成立。
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
history.replaceState(null, '', `#${props.link.id}`)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<li class="g3-toc-item">
|
||
<a :class="linkClass" :href="`#${link.id}`" @click="onClick">
|
||
<span class="g3-toc-link-text">{{ link.text }}</span>
|
||
</a>
|
||
|
||
<ul v-if="link.children?.length" class="g3-toc-list-with-children">
|
||
<G3TocItem
|
||
v-for="child in link.children"
|
||
:key="child.id"
|
||
:link="child"
|
||
:level="level + 1"
|
||
:active-ids="activeIds"
|
||
@navigate="emit('navigate', $event)"
|
||
/>
|
||
</ul>
|
||
</li>
|
||
</template>
|
||
|