2026-01-29 13:58:06 +08:00
|
|
|
<template>
|
2026-02-18 18:17:14 +08:00
|
|
|
<AppShell
|
|
|
|
|
:main-class="mainClasses"
|
|
|
|
|
:sidebar-class="sidebarClasses"
|
|
|
|
|
:content-class="contentClasses"
|
|
|
|
|
>
|
|
|
|
|
<!-- GLOBAL TEXTURE (Paper Noise) -->
|
|
|
|
|
<div
|
|
|
|
|
class="absolute inset-0 pointer-events-none z-0 opacity-[0.03] mix-blend-multiply fixed"
|
|
|
|
|
:style="{ backgroundImage: `url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")` }"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<template #sidebar>
|
|
|
|
|
<!-- HEADER (Brand) -->
|
|
|
|
|
<div class="shrink-0 flex items-center px-6 h-20">
|
2026-01-29 13:58:06 +08:00
|
|
|
<RouterLink
|
|
|
|
|
to="/"
|
2026-02-18 18:17:14 +08:00
|
|
|
class="flex items-center gap-3 group transition-opacity hover:opacity-80"
|
2026-01-29 13:58:06 +08:00
|
|
|
>
|
|
|
|
|
<HeaderLogo
|
2026-02-18 18:17:14 +08:00
|
|
|
size="h-9 w-9"
|
2026-01-29 13:58:06 +08:00
|
|
|
class-name="text-[#191919] dark:text-white"
|
|
|
|
|
/>
|
|
|
|
|
<div class="flex flex-col justify-center">
|
2026-02-18 18:17:14 +08:00
|
|
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
2026-02-13 21:50:05 +08:00
|
|
|
{{ siteName }}
|
2026-01-29 13:58:06 +08:00
|
|
|
</h1>
|
2026-02-18 18:17:14 +08:00
|
|
|
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
|
2026-01-29 13:58:06 +08:00
|
|
|
</div>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-18 18:17:14 +08:00
|
|
|
<!-- NAVIGATION -->
|
|
|
|
|
<div class="flex-1 overflow-y-auto py-2 scrollbar-none">
|
|
|
|
|
<nav class="w-full px-3">
|
|
|
|
|
<div class="space-y-0.5">
|
|
|
|
|
<template
|
2026-06-30 14:14:12 +08:00
|
|
|
v-for="item in resolvedGuideNavItems"
|
2026-01-29 13:58:06 +08:00
|
|
|
:key="item.id"
|
|
|
|
|
>
|
2026-02-18 18:17:14 +08:00
|
|
|
<RouterLink
|
|
|
|
|
:to="item.path"
|
|
|
|
|
class="group relative flex items-center justify-between px-2.5 py-2 rounded-lg transition-all duration-200"
|
|
|
|
|
:class="[
|
|
|
|
|
isNavActive(item.path)
|
|
|
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
|
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
|
|
|
]"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-2.5">
|
|
|
|
|
<component
|
|
|
|
|
:is="item.icon"
|
|
|
|
|
class="h-4 w-4 transition-colors duration-200"
|
|
|
|
|
:class="isNavActive(item.path) ? 'text-primary' : 'text-muted-foreground/70 group-hover:text-foreground'"
|
|
|
|
|
:stroke-width="isNavActive(item.path) ? 2 : 1.75"
|
|
|
|
|
/>
|
|
|
|
|
<span class="text-[13px] tracking-tight">{{ item.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-if="isNavActive(item.path)"
|
|
|
|
|
class="w-1 h-1 rounded-full bg-primary"
|
|
|
|
|
/>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
2026-02-23 15:50:24 +08:00
|
|
|
<!-- 子导航 -->
|
2026-02-18 18:17:14 +08:00
|
|
|
<div
|
2026-02-23 15:50:24 +08:00
|
|
|
v-if="item.subItems && isNavActive(item.path)"
|
|
|
|
|
class="ml-7 space-y-0.5 mt-0.5 mb-2"
|
2026-02-18 18:17:14 +08:00
|
|
|
>
|
|
|
|
|
<a
|
2026-02-23 15:50:24 +08:00
|
|
|
v-for="sub in item.subItems"
|
2026-02-18 18:17:14 +08:00
|
|
|
:key="sub.hash"
|
|
|
|
|
:href="sub.hash"
|
|
|
|
|
class="flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[12px] transition-colors"
|
|
|
|
|
:class="activeHash === sub.hash
|
|
|
|
|
? 'text-primary font-medium'
|
|
|
|
|
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted/30'"
|
|
|
|
|
@click.prevent="scrollToHash(sub.hash)"
|
2026-01-29 13:58:06 +08:00
|
|
|
>
|
2026-02-18 18:17:14 +08:00
|
|
|
<span
|
|
|
|
|
class="w-1 h-1 rounded-full flex-shrink-0"
|
|
|
|
|
:class="activeHash === sub.hash ? 'bg-primary' : 'bg-muted-foreground/30'"
|
|
|
|
|
/>
|
|
|
|
|
{{ sub.name }}
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- FOOTER (Base URL) -->
|
|
|
|
|
<div class="p-4 border-t border-[#3d3929]/5 dark:border-white/5">
|
|
|
|
|
<label class="block text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.1em] mb-2">
|
|
|
|
|
Base URL
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="baseUrl"
|
|
|
|
|
type="text"
|
|
|
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-[#3d3929]/5 dark:border-white/5 bg-white/50 dark:bg-white/5 text-[#191919] dark:text-white placeholder-[#91918d] focus:outline-none focus:ring-2 focus:ring-[#cc785c]/30 transition"
|
|
|
|
|
placeholder="https://your-aether.com"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template #header>
|
|
|
|
|
<!-- Mobile Header -->
|
|
|
|
|
<header class="lg:hidden fixed top-0 left-0 right-0 z-50 border-b border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl transition-all">
|
|
|
|
|
<div class="mx-auto max-w-7xl px-6 py-4">
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<RouterLink
|
|
|
|
|
to="/"
|
|
|
|
|
class="flex items-center gap-3 group"
|
|
|
|
|
>
|
|
|
|
|
<HeaderLogo
|
|
|
|
|
size="h-9 w-9"
|
|
|
|
|
class-name="text-[#191919] dark:text-white"
|
|
|
|
|
/>
|
|
|
|
|
<div class="flex flex-col justify-center">
|
|
|
|
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
|
|
|
|
{{ siteName }}
|
|
|
|
|
</h1>
|
|
|
|
|
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
|
2026-01-29 13:58:06 +08:00
|
|
|
</div>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
2026-02-18 18:17:14 +08:00
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<button
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
2026-06-30 14:14:12 +08:00
|
|
|
:title="themeModeTitle"
|
2026-02-18 18:17:14 +08:00
|
|
|
@click="toggleDarkMode"
|
|
|
|
|
>
|
|
|
|
|
<SunMoon
|
|
|
|
|
v-if="themeMode === 'system'"
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
/>
|
|
|
|
|
<Sun
|
|
|
|
|
v-else-if="themeMode === 'light'"
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
/>
|
|
|
|
|
<Moon
|
|
|
|
|
v-else
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
2026-06-30 14:14:12 +08:00
|
|
|
<LanguageSwitcher />
|
2026-02-18 18:17:14 +08:00
|
|
|
<button
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
|
|
|
|
@click="mobileMenuOpen = !mobileMenuOpen"
|
|
|
|
|
>
|
|
|
|
|
<div class="relative w-5 h-5">
|
|
|
|
|
<Transition
|
|
|
|
|
enter-active-class="transition-all duration-200 ease-out"
|
|
|
|
|
enter-from-class="opacity-0 rotate-90 scale-75"
|
|
|
|
|
enter-to-class="opacity-100 rotate-0 scale-100"
|
|
|
|
|
leave-active-class="transition-all duration-150 ease-in absolute inset-0"
|
|
|
|
|
leave-from-class="opacity-100 rotate-0 scale-100"
|
|
|
|
|
leave-to-class="opacity-0 -rotate-90 scale-75"
|
|
|
|
|
mode="out-in"
|
|
|
|
|
>
|
|
|
|
|
<Menu
|
|
|
|
|
v-if="!mobileMenuOpen"
|
|
|
|
|
class="h-5 w-5"
|
|
|
|
|
/>
|
|
|
|
|
<X
|
|
|
|
|
v-else
|
|
|
|
|
class="h-5 w-5"
|
|
|
|
|
/>
|
|
|
|
|
</Transition>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-01-29 13:58:06 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-18 18:17:14 +08:00
|
|
|
<!-- Mobile Dropdown Menu -->
|
|
|
|
|
<Transition
|
|
|
|
|
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
|
|
|
|
|
enter-from-class="opacity-0 max-h-0"
|
|
|
|
|
enter-to-class="opacity-100 max-h-[500px]"
|
|
|
|
|
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
|
|
|
|
|
leave-from-class="opacity-100 max-h-[500px]"
|
|
|
|
|
leave-to-class="opacity-0 max-h-0"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
v-if="mobileMenuOpen"
|
|
|
|
|
class="border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
|
|
|
|
|
>
|
|
|
|
|
<div class="mx-auto max-w-7xl px-6 py-4">
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<div
|
|
|
|
|
v-for="group in navigation"
|
|
|
|
|
:key="group.title"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
v-if="group.title"
|
|
|
|
|
class="text-[10px] font-semibold text-[#91918d] dark:text-muted-foreground uppercase tracking-wider mb-2"
|
|
|
|
|
>
|
|
|
|
|
{{ group.title }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid grid-cols-2 gap-2">
|
|
|
|
|
<RouterLink
|
|
|
|
|
v-for="item in group.items"
|
|
|
|
|
:key="item.href"
|
|
|
|
|
:to="item.href"
|
|
|
|
|
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all"
|
|
|
|
|
:class="isNavActive(item.href)
|
|
|
|
|
? 'bg-[#cc785c]/10 dark:bg-[#cc785c]/20 text-[#cc785c] dark:text-[#d4a27f]'
|
|
|
|
|
: 'text-[#666663] dark:text-muted-foreground hover:bg-black/5 dark:hover:bg-white/5 hover:text-[#191919] dark:hover:text-white'"
|
|
|
|
|
@click="mobileMenuOpen = false"
|
|
|
|
|
>
|
|
|
|
|
<component
|
|
|
|
|
:is="item.icon"
|
|
|
|
|
class="h-4 w-4 shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
<span class="truncate">{{ item.name }}</span>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Desktop Page Header -->
|
|
|
|
|
<header class="hidden lg:flex h-16 px-8 items-center justify-between shrink-0 border-b border-[#3d3929]/5 dark:border-white/5 sticky top-0 z-40 backdrop-blur-md bg-[#faf9f5]/90 dark:bg-[#191714]/90">
|
|
|
|
|
<div class="flex flex-col gap-0.5">
|
|
|
|
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
<RouterLink
|
|
|
|
|
to="/guide"
|
|
|
|
|
class="hover:text-foreground transition-colors"
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
{{ t('guide.title') }}
|
2026-02-18 18:17:14 +08:00
|
|
|
</RouterLink>
|
|
|
|
|
<template v-if="currentNavItem && currentNavItem.id !== 'overview'">
|
|
|
|
|
<ChevronRight class="w-3 h-3 opacity-50" />
|
|
|
|
|
<span class="text-foreground font-medium">
|
|
|
|
|
{{ currentNavItem.name }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
2026-01-29 13:58:06 +08:00
|
|
|
</div>
|
2026-02-18 18:17:14 +08:00
|
|
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
2026-06-30 14:14:12 +08:00
|
|
|
:title="themeModeTitle"
|
2026-02-18 18:17:14 +08:00
|
|
|
@click="toggleDarkMode"
|
|
|
|
|
>
|
|
|
|
|
<SunMoon
|
|
|
|
|
v-if="themeMode === 'system'"
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
/>
|
|
|
|
|
<Sun
|
|
|
|
|
v-else-if="themeMode === 'light'"
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
/>
|
|
|
|
|
<Moon
|
|
|
|
|
v-else
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
<a
|
|
|
|
|
href="https://github.com/fawney19/Aether"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
2026-06-30 14:14:12 +08:00
|
|
|
:title="t('common.githubRepository')"
|
2026-02-18 18:17:14 +08:00
|
|
|
>
|
|
|
|
|
<GithubIcon class="h-4 w-4" />
|
|
|
|
|
</a>
|
2026-06-30 14:14:12 +08:00
|
|
|
<LanguageSwitcher />
|
2026-02-18 18:17:14 +08:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
</template>
|
|
|
|
|
|
2026-02-23 15:50:24 +08:00
|
|
|
<article
|
|
|
|
|
class="max-w-4xl mx-auto pb-24"
|
|
|
|
|
@click="onArticleClick"
|
|
|
|
|
>
|
2026-02-18 18:17:14 +08:00
|
|
|
<RouterView
|
|
|
|
|
v-slot="{ Component }"
|
|
|
|
|
>
|
2026-02-23 15:50:24 +08:00
|
|
|
<transition
|
|
|
|
|
name="fade"
|
|
|
|
|
mode="out-in"
|
|
|
|
|
>
|
|
|
|
|
<component
|
|
|
|
|
:is="Component"
|
|
|
|
|
:base-url="baseUrl"
|
|
|
|
|
class="literary-content"
|
|
|
|
|
/>
|
|
|
|
|
</transition>
|
2026-02-18 18:17:14 +08:00
|
|
|
</RouterView>
|
2026-02-23 15:50:24 +08:00
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
<!-- Image Lightbox -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<Transition
|
|
|
|
|
enter-active-class="transition duration-200 ease-out"
|
|
|
|
|
enter-from-class="opacity-0"
|
|
|
|
|
enter-to-class="opacity-100"
|
|
|
|
|
leave-active-class="transition duration-150 ease-in"
|
|
|
|
|
leave-from-class="opacity-100"
|
|
|
|
|
leave-to-class="opacity-0"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
v-if="lightboxSrc"
|
|
|
|
|
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm cursor-zoom-out"
|
|
|
|
|
@click="lightboxSrc = ''"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
:src="lightboxSrc"
|
|
|
|
|
:alt="lightboxAlt"
|
|
|
|
|
class="max-w-[90vw] max-h-[90vh] object-contain rounded-xl shadow-2xl"
|
|
|
|
|
@click.stop
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</Teleport>
|
2026-02-18 18:17:14 +08:00
|
|
|
</AppShell>
|
2026-01-29 13:58:06 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-02-23 15:50:24 +08:00
|
|
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
2026-01-29 13:58:06 +08:00
|
|
|
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
|
|
|
|
import {
|
|
|
|
|
Menu,
|
|
|
|
|
Moon,
|
|
|
|
|
Sun,
|
2026-02-18 18:17:14 +08:00
|
|
|
SunMoon,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
X
|
2026-01-29 13:58:06 +08:00
|
|
|
} from 'lucide-vue-next'
|
|
|
|
|
import GithubIcon from '@/components/icons/GithubIcon.vue'
|
|
|
|
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
2026-06-30 14:14:12 +08:00
|
|
|
import LanguageSwitcher from '@/components/common/LanguageSwitcher.vue'
|
2026-02-18 18:17:14 +08:00
|
|
|
import AppShell from '@/components/layout/AppShell.vue'
|
2026-01-29 13:58:06 +08:00
|
|
|
import { useDarkMode } from '@/composables/useDarkMode'
|
2026-02-13 21:50:05 +08:00
|
|
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
2026-06-30 14:14:12 +08:00
|
|
|
import { useI18n } from '@/i18n'
|
2026-01-29 13:58:06 +08:00
|
|
|
import { guideNavItems } from './guide-config'
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const { themeMode, toggleDarkMode } = useDarkMode()
|
2026-02-13 21:50:05 +08:00
|
|
|
const { siteName, siteSubtitle } = useSiteInfo()
|
2026-06-30 14:14:12 +08:00
|
|
|
const { t } = useI18n()
|
2026-01-29 13:58:06 +08:00
|
|
|
|
2026-02-18 18:17:14 +08:00
|
|
|
const mobileMenuOpen = ref(false)
|
2026-01-29 13:58:06 +08:00
|
|
|
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')
|
2026-02-23 15:50:24 +08:00
|
|
|
const activeHash = ref('')
|
|
|
|
|
const lightboxSrc = ref('')
|
|
|
|
|
const lightboxAlt = ref('')
|
2026-06-30 14:14:12 +08:00
|
|
|
const themeModeTitle = computed(() => {
|
|
|
|
|
if (themeMode.value === 'system') return t('theme.system')
|
|
|
|
|
if (themeMode.value === 'dark') return t('theme.dark')
|
|
|
|
|
return t('theme.light')
|
|
|
|
|
})
|
|
|
|
|
const resolvedGuideNavItems = computed(() => guideNavItems.map(item => ({
|
|
|
|
|
...item,
|
|
|
|
|
name: t(item.nameKey),
|
|
|
|
|
description: item.descriptionKey ? t(item.descriptionKey) : undefined,
|
|
|
|
|
subItems: item.subItems?.map(subItem => ({
|
|
|
|
|
...subItem,
|
|
|
|
|
name: t(subItem.nameKey),
|
|
|
|
|
})),
|
|
|
|
|
})))
|
2026-02-18 18:17:14 +08:00
|
|
|
|
2026-02-23 15:50:24 +08:00
|
|
|
function onArticleClick(e: MouseEvent) {
|
|
|
|
|
const target = e.target as HTMLElement
|
|
|
|
|
if (target.tagName === 'IMG' && target.closest('.literary-content')) {
|
|
|
|
|
const img = target as HTMLImageElement
|
|
|
|
|
lightboxSrc.value = img.src
|
|
|
|
|
lightboxAlt.value = img.alt || ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let observer: IntersectionObserver | null = null
|
|
|
|
|
|
|
|
|
|
function getScrollContainer(): Element | null {
|
|
|
|
|
return document.querySelector('.app-shell__content')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupIntersectionObserver() {
|
|
|
|
|
if (observer) {
|
|
|
|
|
observer.disconnect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scrollRoot = getScrollContainer()
|
|
|
|
|
|
|
|
|
|
observer = new IntersectionObserver(
|
|
|
|
|
(entries) => {
|
|
|
|
|
const visibleEntries = entries.filter((entry) => entry.isIntersecting)
|
|
|
|
|
if (visibleEntries.length > 0) {
|
|
|
|
|
const topEntry = visibleEntries.reduce((prev, current) => {
|
|
|
|
|
return (current.boundingClientRect.top < prev.boundingClientRect.top) ? current : prev
|
|
|
|
|
})
|
|
|
|
|
activeHash.value = `#${topEntry.target.id}`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
root: scrollRoot,
|
|
|
|
|
rootMargin: '-80px 0px -70% 0px',
|
|
|
|
|
threshold: 0
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const sections = document.querySelectorAll('article section[id]')
|
|
|
|
|
sections.forEach((section) => observer?.observe(section))
|
|
|
|
|
}
|
2026-02-18 18:17:14 +08:00
|
|
|
|
|
|
|
|
function scrollToHash(hash: string) {
|
|
|
|
|
activeHash.value = hash
|
|
|
|
|
const el = document.querySelector(hash)
|
2026-02-23 15:50:24 +08:00
|
|
|
const container = getScrollContainer()
|
|
|
|
|
if (el && container) {
|
|
|
|
|
const elTop = el.getBoundingClientRect().top
|
|
|
|
|
const containerTop = container.getBoundingClientRect().top
|
|
|
|
|
const offset = elTop - containerTop + container.scrollTop - 80
|
|
|
|
|
container.scrollTo({ top: offset, behavior: 'smooth' })
|
2026-02-18 18:17:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 15:50:24 +08:00
|
|
|
// 路由变化时管理状态和观察者
|
|
|
|
|
watch(
|
|
|
|
|
() => route.path,
|
|
|
|
|
() => {
|
|
|
|
|
mobileMenuOpen.value = false
|
|
|
|
|
activeHash.value = ''
|
2026-02-24 12:15:14 +08:00
|
|
|
const container = getScrollContainer()
|
|
|
|
|
if (container) {
|
|
|
|
|
container.scrollTo({ top: 0 })
|
|
|
|
|
}
|
2026-02-23 15:50:24 +08:00
|
|
|
nextTick(() => {
|
|
|
|
|
setupIntersectionObserver()
|
|
|
|
|
const firstSection = document.querySelector('article section[id]')
|
|
|
|
|
if (!activeHash.value && firstSection) {
|
|
|
|
|
activeHash.value = `#${firstSection.id}`
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
setupIntersectionObserver()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (observer) {
|
|
|
|
|
observer.disconnect()
|
|
|
|
|
}
|
2026-02-18 18:17:14 +08:00
|
|
|
})
|
2026-01-29 13:58:06 +08:00
|
|
|
|
|
|
|
|
const currentNavItem = computed(() => {
|
2026-06-30 14:14:12 +08:00
|
|
|
return resolvedGuideNavItems.value.find(item => item.path === route.path)
|
2026-01-29 13:58:06 +08:00
|
|
|
})
|
|
|
|
|
|
2026-02-18 18:17:14 +08:00
|
|
|
function isNavActive(href: string) {
|
|
|
|
|
if (href === '/guide') {
|
2026-01-29 13:58:06 +08:00
|
|
|
return route.path === '/guide'
|
|
|
|
|
}
|
2026-02-18 18:17:14 +08:00
|
|
|
return route.path === href || route.path.startsWith(`${href}/`)
|
2026-01-29 13:58:06 +08:00
|
|
|
}
|
2026-02-18 18:17:14 +08:00
|
|
|
|
|
|
|
|
// 移动端菜单用的导航数据
|
|
|
|
|
const navigation = computed(() => [
|
|
|
|
|
{
|
2026-02-23 15:50:24 +08:00
|
|
|
title: '',
|
2026-06-30 14:14:12 +08:00
|
|
|
items: resolvedGuideNavItems.value.map(item => ({
|
2026-02-18 18:17:14 +08:00
|
|
|
name: item.name,
|
|
|
|
|
href: item.path,
|
|
|
|
|
icon: item.icon
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// 样式类 - 与 MainLayout 保持一致
|
|
|
|
|
const sidebarClasses = computed(() => {
|
|
|
|
|
return 'w-[260px] flex flex-col hidden lg:flex border-r border-[#3d3929]/5 dark:border-white/5 bg-[#faf9f5] dark:bg-[#1e1c19] h-screen sticky top-0'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const contentClasses = computed(() => {
|
|
|
|
|
return 'flex-1 min-w-0 bg-[#faf9f5] dark:bg-[#191714] text-[#3d3929] dark:text-[#d4a27f]'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const mainClasses = computed(() => {
|
2026-02-23 15:50:24 +08:00
|
|
|
return 'pt-24 lg:pt-8'
|
2026-02-18 18:17:14 +08:00
|
|
|
})
|
2026-01-29 13:58:06 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-02-18 18:17:14 +08:00
|
|
|
.scrollbar-none::-webkit-scrollbar { display: none; }
|
|
|
|
|
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
|
2026-02-23 15:50:24 +08:00
|
|
|
|
|
|
|
|
/* Literary Tech Typography Overrides for Guide Content */
|
|
|
|
|
:deep(.literary-content) h2 {
|
|
|
|
|
@apply text-2xl mb-8 mt-12 flex items-center gap-3 transition-colors;
|
|
|
|
|
font-family: var(--serif);
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
letter-spacing: -0.015em;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.literary-content) h3 {
|
|
|
|
|
@apply text-xl mb-6 mt-10 transition-colors;
|
|
|
|
|
font-family: var(--serif);
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
letter-spacing: -0.01em;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.literary-content) p:not([class*="text-sm"]):not([class*="text-xs"]) {
|
|
|
|
|
font-family: var(--serif);
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
@apply leading-relaxed text-[1.05rem] mb-4;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.literary-content) li:not([class*="text-sm"]):not([class*="text-xs"]) {
|
|
|
|
|
font-family: var(--serif);
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
@apply leading-relaxed text-[1.05rem] mb-2;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* UI Elements inside content should remain sans-serif */
|
|
|
|
|
:deep(.literary-content) button,
|
|
|
|
|
:deep(.literary-content) input,
|
|
|
|
|
:deep(.literary-content) select,
|
|
|
|
|
:deep(.literary-content) label,
|
|
|
|
|
:deep(.literary-content) table,
|
|
|
|
|
:deep(.literary-content) .font-mono,
|
|
|
|
|
:deep(.literary-content) [class*="font-mono"] {
|
|
|
|
|
font-family: var(--sans-serif);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.literary-content) pre,
|
|
|
|
|
:deep(.literary-content) code {
|
|
|
|
|
font-family: var(--monospace) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.literary-content) img {
|
|
|
|
|
cursor: zoom-in;
|
|
|
|
|
}
|
2026-01-29 13:58:06 +08:00
|
|
|
</style>
|