135 lines
4.1 KiB
Vue
135 lines
4.1 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 侧边栏条目 —— DOM 与类名照抄 Docus / @nuxt/ui 的 ContentNavigation。
|
||
*
|
||
* 结构对照(取自 docus.dev 实测 DOM):
|
||
* 有子项: li.itemWithChildren > button.link.trigger + div.content > ul.listWithChildren
|
||
* 无子项: li.item > a.link
|
||
*
|
||
* 实测样式(docus.dev,1440px,light):
|
||
* ul.list margin: -6px -10px 0 (-mt-1.5 -mx-2.5)
|
||
* li.itemWithChildren display:flex; flex-direction:column
|
||
* button/a.link padding:6px 10px; gap:6px; font-size:14px; border-radius:0
|
||
* 高度 32px(py-1.5*2 + 行高 20)
|
||
* 圆角 6px 在 ::before 上(before:rounded-md)
|
||
* 分组标题(trigger) font-weight:600; color: text-highlighted
|
||
* 普通项 font-weight:400; color: text-muted
|
||
* 激活项 font-weight:500; color: text-primary;无背景
|
||
* ::after 竖线 w=1px h=28px left=-6px top/bottom=2px bg=primary
|
||
* 图标 width/height:16px(size-4);margin-inline:2px(mx-0.5)
|
||
* ul.listWithChildren margin-inline-start:20px; border-inline-start:1px;
|
||
* padding-inline-start:0
|
||
*/
|
||
import { computed, ref } from 'vue'
|
||
import { useData } from 'vitepress'
|
||
import { isActive } from 'vitepress/dist/client/shared.js'
|
||
import { sidebarIcons } from './sidebarIcons'
|
||
|
||
interface G3SidebarItem {
|
||
text?: string
|
||
link?: string
|
||
/** 图标名(对应 sidebarIcons 的键) */
|
||
icon?: string
|
||
rel?: string
|
||
target?: string
|
||
collapsed?: boolean
|
||
items?: G3SidebarItem[]
|
||
}
|
||
|
||
const props = defineProps<{
|
||
item: G3SidebarItem
|
||
level: number
|
||
}>()
|
||
|
||
const { page } = useData()
|
||
|
||
/** 该条目自身是否就是当前页 */
|
||
const isActiveLink = computed(() =>
|
||
props.item.link ? isActive(page.value.relativePath, props.item.link) : false,
|
||
)
|
||
|
||
function treeHasActive(item: G3SidebarItem): boolean {
|
||
if (item.link && isActive(page.value.relativePath, item.link)) return true
|
||
return (item.items ?? []).some(treeHasActive)
|
||
}
|
||
|
||
const hasActiveLink = computed(() => treeHasActive(props.item))
|
||
const hasChildren = computed(() => !!(props.item.items && props.item.items.length))
|
||
|
||
/** 折叠状态:local 覆盖 + 子项含当前页时强制展开 */
|
||
const overridden = ref<boolean | null>(null)
|
||
const collapsed = computed(() => {
|
||
if (overridden.value !== null) return overridden.value
|
||
if (hasActiveLink.value) return false
|
||
return !!props.item.collapsed
|
||
})
|
||
|
||
function toggle() {
|
||
overridden.value = !collapsed.value
|
||
}
|
||
|
||
const iconComponent = computed(() =>
|
||
props.item.icon ? sidebarIcons[props.item.icon] ?? null : null,
|
||
)
|
||
|
||
const linkClass = computed(() => [
|
||
'g3-nav-link',
|
||
// trigger(分组标题):font-semibold
|
||
hasChildren.value ? 'g3-nav-trigger' : '',
|
||
isActiveLink.value ? 'is-active' : '',
|
||
{ 'g3-nav-disabled': false },
|
||
])
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 有子项:li.itemWithChildren > button.link + div.content > ul.listWithChildren -->
|
||
<li v-if="hasChildren" class="g3-item-with-children">
|
||
<button
|
||
type="button"
|
||
:class="linkClass"
|
||
:aria-expanded="!collapsed"
|
||
@click="toggle"
|
||
>
|
||
<component
|
||
:is="iconComponent"
|
||
v-if="iconComponent"
|
||
class="g3-nav-icon"
|
||
:size="16"
|
||
aria-hidden="true"
|
||
/>
|
||
<span class="g3-nav-title">{{ item.text }}</span>
|
||
</button>
|
||
|
||
<div v-show="!collapsed" class="g3-nav-content">
|
||
<ul class="g3-list-with-children">
|
||
<G3SidebarItem
|
||
v-for="(child, i) in item.items"
|
||
:key="`${i}-${child.text}`"
|
||
:item="child"
|
||
:level="level + 1"
|
||
/>
|
||
</ul>
|
||
</div>
|
||
</li>
|
||
|
||
<!-- 无子项:li.item > a.link -->
|
||
<li v-else class="g3-item">
|
||
<a
|
||
:class="linkClass"
|
||
:href="item.link"
|
||
:rel="item.rel"
|
||
:target="item.target"
|
||
:aria-current="isActiveLink ? 'page' : undefined"
|
||
>
|
||
<component
|
||
:is="iconComponent"
|
||
v-if="iconComponent"
|
||
class="g3-nav-icon"
|
||
:size="16"
|
||
aria-hidden="true"
|
||
/>
|
||
<span class="g3-nav-title">{{ item.text }}</span>
|
||
</a>
|
||
</li>
|
||
</template>
|