refactor(guide): 重构指南页面为 Markdown 内容驱动架构

- 将各指南页面的内联硬编码内容抽取为独立 Markdown 文件
- 新增 MarkdownViewer 组件统一渲染 Markdown 内容
- 新增 ArchitectureDiagram 组件和架构演示组件
- 新增模块指南页面 (ModulesGuide)
- GuideLayout 支持子导航、页面过渡动画和图片 Lightbox
- guide-config 重构为支持 subItems 的导航结构
- 添加指南相关截图资源
This commit is contained in:
fawney19
2026-02-23 15:50:24 +08:00
parent c43d22b0e4
commit e18a7ee435
49 changed files with 2988 additions and 4735 deletions
+178 -25
View File
@@ -62,13 +62,13 @@
/>
</RouterLink>
<!-- 快速开始子导航 -->
<!-- 子导航 -->
<div
v-if="item.id === 'overview' && isNavActive(item.path)"
class="ml-7 space-y-0.5 mt-0.5"
v-if="item.subItems && isNavActive(item.path)"
class="ml-7 space-y-0.5 mt-0.5 mb-2"
>
<a
v-for="sub in overviewSubItems"
v-for="sub in item.subItems"
:key="sub.hash"
:href="sub.hash"
class="flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[12px] transition-colors"
@@ -273,21 +273,55 @@
</header>
</template>
<div class="max-w-4xl mx-auto">
<article
class="max-w-4xl mx-auto pb-24"
@click="onArticleClick"
>
<RouterView
v-slot="{ Component }"
>
<component
:is="Component"
:base-url="baseUrl"
/>
<transition
name="fade"
mode="out-in"
>
<component
:is="Component"
:base-url="baseUrl"
class="literary-content"
/>
</transition>
</RouterView>
</div>
</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>
</AppShell>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
Menu,
@@ -310,27 +344,92 @@ const { siteName, siteSubtitle } = useSiteInfo()
const mobileMenuOpen = ref(false)
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')
const activeHash = ref('#production')
const activeHash = ref('')
const lightboxSrc = ref('')
const lightboxAlt = ref('')
// 快速开始子导航
const overviewSubItems = [
{ name: '部署', hash: '#production' },
{ name: '配置流程', hash: '#config-steps' },
{ name: 'API 格式', hash: '#api-formats' },
{ name: '推荐帖子', hash: '#recommended-posts' }
]
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))
}
function scrollToHash(hash: string) {
activeHash.value = hash
const el = document.querySelector(hash)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
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' })
}
}
// 路由变化时关闭移动端菜单
watch(() => route.path, () => {
mobileMenuOpen.value = false
// 路由变化时管理状态和观察者
watch(
() => route.path,
() => {
mobileMenuOpen.value = false
activeHash.value = ''
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()
}
})
const currentNavItem = computed(() => {
@@ -347,6 +446,7 @@ function isNavActive(href: string) {
// 移动端菜单用的导航数据
const navigation = computed(() => [
{
title: '',
items: guideNavItems.map(item => ({
name: item.name,
href: item.path,
@@ -365,11 +465,64 @@ const contentClasses = computed(() => {
})
const mainClasses = computed(() => {
return 'pt-24 lg:pt-6'
return 'pt-24 lg:pt-8'
})
</script>
<style scoped>
.scrollbar-none::-webkit-scrollbar { display: none; }
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
/* 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;
}
</style>