数据结构优化

This commit is contained in:
李琦
2026-01-20 16:59:40 +08:00
parent 524ce8cf79
commit cf8b89f731

View File

@@ -1,18 +1,18 @@
<template> <template>
<section id="blog-detail" class="page-section block"> <section id="blog-detail" class="page-section block">
<div class="max-w-[90rem] mx-auto pt-32 px-4 md:px-6 pb-20 relative"> <div class="max-w-[90rem] mx-auto pt-20 md:pt-32 px-4 md:px-6 pb-12 md:pb-20 relative overflow-hidden">
<!-- 返回按钮 --> <!-- 返回按钮 -->
<button <button
@click="$router.push('/blog')" @click="$router.push('/blog')"
class="group flex items-center gap-2 text-art-muted hover:text-white transition-colors mb-8 md:mb-12" class="group flex items-center gap-2 text-art-muted hover:text-white transition-colors mb-6 md:mb-12"
> >
<Icon name="arrow-left" :size="16" class="transform group-hover:-translate-x-1 transition-transform" /> <Icon name="arrow-left" :size="16" class="transform group-hover:-translate-x-1 transition-transform" />
<span class="text-sm font-medium tracking-wide">返回文章列表</span> <span class="text-sm font-medium tracking-wide">返回列表</span>
</button> </button>
<!-- Loading state --> <!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center h-64"> <div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div> <div class="animate-spin rounded-full h-10 w-10 md:h-12 md:w-12 border-t-2 border-b-2 border-art-accent"></div>
</div> </div>
<!-- Error state --> <!-- Error state -->
@@ -98,14 +98,14 @@
<!-- 2. 中间文章主内容 --> <!-- 2. 中间文章主内容 -->
<div class="flex-1 min-w-0 order-2"> <div class="flex-1 min-w-0 order-2">
<!-- 文章头图与信息 --> <!-- 文章头图与信息 -->
<div class="border-b border-white/5 pb-8 mb-10"> <div class="border-b border-white/5 pb-6 md:pb-8 mb-6 md:mb-10">
<div class="flex flex-wrap items-center gap-3 mb-6"> <div class="flex flex-wrap items-center gap-2 md:gap-3 mb-4 md:mb-6">
<span class="px-3 py-1 border border-white/20 rounded-full text-xs font-mono text-art-accent uppercase tracking-wider">{{ post.categoryName || '未分类' }}</span> <span class="px-2 py-0.5 md:px-3 md:py-1 border border-white/20 rounded-full text-[10px] md:text-xs font-mono text-art-accent uppercase tracking-wider">{{ post.categoryName || '未分类' }}</span>
<span class="text-sm text-art-muted">{{ post.date }}</span> <span class="text-xs md:text-sm text-art-muted">{{ post.date }}</span>
</div> </div>
<h1 class="font-serif text-3xl md:text-5xl lg:text-6xl text-white leading-tight mb-8 break-words">{{ post.title }}</h1> <h1 class="font-serif text-2xl md:text-5xl lg:text-6xl text-white leading-snug md:leading-tight mb-6 md:mb-8 break-words">{{ post.title }}</h1>
<div class="flex items-center gap-4"> <div class="flex items-center gap-3 md:gap-4">
<div class="w-10 h-10 rounded-full overflow-hidden border border-white/20 flex-shrink-0"> <div class="w-8 h-8 md:w-10 md:h-10 rounded-full overflow-hidden border border-white/20 flex-shrink-0">
<img <img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=NianGao" src="https://api.dicebear.com/7.x/avataaars/svg?seed=NianGao"
alt="Avatar" alt="Avatar"
@@ -113,8 +113,8 @@
> >
</div> </div>
<div> <div>
<div class="text-sm font-medium text-white">年糕崽崽</div> <div class="text-xs md:text-sm font-medium text-white">年糕崽崽</div>
<div class="text-xs text-art-muted">前端架构师</div> <div class="text-[10px] md:text-xs text-art-muted">前端架构师</div>
</div> </div>
</div> </div>
</div> </div>
@@ -128,10 +128,9 @@
</article> </article>
<!-- 移动端专属信息补充区 --> <!-- 移动端专属信息补充区 -->
<div class="xl:hidden mt-12 border-t border-white/10 pt-8 space-y-6"> <div class="xl:hidden mt-8 md:mt-12 border-t border-white/10 pt-6 md:pt-8 space-y-4 md:space-y-6">
<!-- 标签补充 -->
<div v-if="post.tags && post.tags.length > 0"> <div v-if="post.tags && post.tags.length > 0">
<h4 class="text-xs font-bold text-white uppercase tracking-wider mb-3 flex items-center gap-2"> <h4 class="text-xs font-bold text-white uppercase tracking-wider mb-2 md:mb-3 flex items-center gap-2">
<Icon name="tags" :size="14" class="text-art-accent" /> <Icon name="tags" :size="14" class="text-art-accent" />
标签 标签
</h4> </h4>
@@ -141,20 +140,19 @@
:key="tag.id" :key="tag.id"
@click.prevent="handleTagClick(tag.id)" @click.prevent="handleTagClick(tag.id)"
href="#" href="#"
class="px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-xs text-art-muted hover:text-white hover:bg-white/10 transition-colors" class="px-2.5 py-1 rounded-full bg-white/5 border border-white/10 text-xs text-art-muted hover:text-white hover:bg-white/10 transition-colors"
> >
# {{ tag.name }} # {{ tag.name }}
</a> </a>
</div> </div>
</div> </div>
<!-- 专栏/分类补充 --> <div class="flex flex-wrap gap-3 md:gap-4">
<div class="flex flex-wrap gap-4"> <div v-if="post.columnName" class="flex items-center gap-2 text-xs md:text-sm text-art-muted">
<div v-if="post.columnName" class="flex items-center gap-2 text-sm text-art-muted">
<Icon name="book-open" :size="14" class="text-art-accent" /> <Icon name="book-open" :size="14" class="text-art-accent" />
专栏: <span class="text-white">{{ post.columnName }}</span> 专栏: <span class="text-white">{{ post.columnName }}</span>
</div> </div>
<div v-if="post.categoryName" class="flex items-center gap-2 text-sm text-art-muted"> <div v-if="post.categoryName" class="flex items-center gap-2 text-xs md:text-sm text-art-muted">
<Icon name="folder" :size="14" class="text-art-accent" /> <Icon name="folder" :size="14" class="text-art-accent" />
分类: <span class="text-white">{{ post.categoryName }}</span> 分类: <span class="text-white">{{ post.categoryName }}</span>
</div> </div>
@@ -162,9 +160,9 @@
</div> </div>
<!-- 底部推荐阅读 --> <!-- 底部推荐阅读 -->
<div class="mt-12 md:mt-20 border-t border-white/10 pt-10"> <div class="mt-8 md:mt-20 border-t border-white/10 pt-8 md:pt-10">
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-2"> <h3 class="text-base md:text-lg font-bold text-white mb-4 md:mb-6 flex items-center gap-2">
<Icon name="sparkles" :size="20" class="text-art-accent" /> <Icon name="sparkles" :size="18" class="text-art-accent" />
推荐阅读 推荐阅读
</h3> </h3>
<div v-if="loadingRecommendations" class="flex justify-center py-4"> <div v-if="loadingRecommendations" class="flex justify-center py-4">
@@ -173,7 +171,7 @@
<div v-else-if="recommendedPosts.length === 0" class="text-sm text-art-muted italic"> <div v-else-if="recommendedPosts.length === 0" class="text-sm text-art-muted italic">
暂无推荐 暂无推荐
</div> </div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
<router-link <router-link
v-for="recPost in recommendedPosts" v-for="recPost in recommendedPosts"
:key="recPost.id" :key="recPost.id"
@@ -185,13 +183,13 @@
:href="href" :href="href"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="block p-5 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-300 group h-full" class="block p-4 md:p-5 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-300 group h-full"
> >
<div class="text-base font-medium text-white group-hover:text-art-accent transition-colors mb-3 line-clamp-2"> <div class="text-sm md:text-base font-medium text-white group-hover:text-art-accent transition-colors mb-2 md:mb-3 line-clamp-2">
{{ recPost.title }} {{ recPost.title }}
</div> </div>
<div class="flex items-center justify-between text-xs text-art-muted mt-auto"> <div class="flex items-center justify-between text-[10px] md:text-xs text-art-muted mt-auto">
<span v-if="recPost.categoryName" class="px-2 py-0.5 rounded border border-white/20"> <span v-if="recPost.categoryName" class="px-1.5 py-0.5 rounded border border-white/20">
{{ recPost.categoryName }} {{ recPost.categoryName }}
</span> </span>
<span>{{ recPost.date }}</span> <span>{{ recPost.date }}</span>
@@ -204,14 +202,12 @@
<!-- 3. 右侧边栏目录 (TOC) - 仅在大屏显示 --> <!-- 3. 右侧边栏目录 (TOC) - 仅在大屏显示 -->
<aside class="hidden xl:block w-64 flex-shrink-0 order-3 sticky top-32 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar"> <aside class="hidden xl:block w-64 flex-shrink-0 order-3 sticky top-32 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar">
<!-- 目录导航 -->
<div class="p-5 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-white/10"> <div class="p-5 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-white/10">
<h3 class="text-xs font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2"> <h3 class="text-xs font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<Icon name="list" :size="14" class="text-art-accent" /> <Icon name="list" :size="14" class="text-art-accent" />
目录导航 目录导航
</h3> </h3>
<nav class="space-y-1 relative"> <nav class="space-y-1 relative">
<!-- 激活状态的指示条 -->
<div <div
class="absolute left-0 w-0.5 bg-art-accent transition-all duration-300 ease-out" class="absolute left-0 w-0.5 bg-art-accent transition-all duration-300 ease-out"
:style="activeIndicatorStyle" :style="activeIndicatorStyle"
@@ -248,18 +244,15 @@
<button <button
v-if="toc.length > 0 && !loading && !error" v-if="toc.length > 0 && !loading && !error"
@click="showMobileToc = true" @click="showMobileToc = true"
class="xl:hidden fixed bottom-8 right-6 z-50 w-12 h-12 bg-art-accent/90 backdrop-blur text-white rounded-full shadow-lg shadow-art-accent/20 flex items-center justify-center hover:bg-art-accent active:scale-95 transition-all duration-200" class="xl:hidden fixed bottom-6 right-4 z-50 w-10 h-10 md:w-12 md:h-12 bg-art-accent/90 backdrop-blur text-white rounded-full shadow-lg shadow-art-accent/20 flex items-center justify-center hover:bg-art-accent active:scale-95 transition-all duration-200"
aria-label="目录" aria-label="目录"
> >
<Icon name="list" :size="20" /> <Icon name="list" :size="18" />
</button> </button>
<!-- Mobile TOC Drawer --> <!-- Mobile TOC Drawer -->
<div v-if="showMobileToc" class="fixed inset-0 z-[100] xl:hidden"> <div v-if="showMobileToc" class="fixed inset-0 z-[100] xl:hidden">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity" @click="showMobileToc = false"></div> <div class="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity" @click="showMobileToc = false"></div>
<!-- Drawer Content -->
<div class="absolute bottom-0 left-0 right-0 bg-[#1a1a1a] border-t border-white/10 rounded-t-2xl max-h-[70vh] flex flex-col animate-slide-up shadow-2xl"> <div class="absolute bottom-0 left-0 right-0 bg-[#1a1a1a] border-t border-white/10 rounded-t-2xl max-h-[70vh] flex flex-col animate-slide-up shadow-2xl">
<div class="flex items-center justify-between p-4 border-b border-white/10"> <div class="flex items-center justify-between p-4 border-b border-white/10">
<h3 class="text-sm font-bold text-white flex items-center gap-2"> <h3 class="text-sm font-bold text-white flex items-center gap-2">
@@ -303,10 +296,8 @@ import { fetchPost, getRecommendedPosts, Post, getPublicSettings } from '../serv
import { useScrollAnimation } from '../composables/useScrollAnimation' import { useScrollAnimation } from '../composables/useScrollAnimation'
import Icon from '../components/Icon.vue' import Icon from '../components/Icon.vue'
// 引入 Markdown 解析依赖
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js' import hljs from 'highlight.js'
// 引入代码高亮样式 (Atom One Dark)
import 'highlight.js/styles/atom-one-dark.css' import 'highlight.js/styles/atom-one-dark.css'
const route = useRoute() const route = useRoute()
@@ -314,19 +305,16 @@ const router = useRouter()
const postId = route.params.id as string const postId = route.params.id as string
const articleRef = ref<HTMLElement | null>(null) const articleRef = ref<HTMLElement | null>(null)
// 处理分类点击,使用 sessionStorage 传递参数不在URL显示
const handleCategoryClick = (categoryId: number) => { const handleCategoryClick = (categoryId: number) => {
sessionStorage.setItem('blogFilterCategoryId', categoryId.toString()) sessionStorage.setItem('blogFilterCategoryId', categoryId.toString())
router.push('/blog') router.push('/blog')
} }
// 处理标签点击,使用 sessionStorage 传递参数不在URL显示
const handleTagClick = (tagId: number) => { const handleTagClick = (tagId: number) => {
sessionStorage.setItem('blogFilterTagId', tagId.toString()) sessionStorage.setItem('blogFilterTagId', tagId.toString())
router.push('/blog') router.push('/blog')
} }
// 目录相关状态
interface TocItem { interface TocItem {
id: string id: string
text: string text: string
@@ -336,7 +324,6 @@ const toc = ref<TocItem[]>([])
const activeId = ref<string>('') const activeId = ref<string>('')
const observer = ref<IntersectionObserver | null>(null) const observer = ref<IntersectionObserver | null>(null)
const showMobileToc = ref(false) const showMobileToc = ref(false)
// 核心修复 1: 引入点击滚动标志位
const isClickScrolling = ref(false) const isClickScrolling = ref(false)
let scrollTimeout: number | null = null let scrollTimeout: number | null = null
@@ -356,25 +343,21 @@ const loadingRecommendations = ref(false)
const { initObserver } = useScrollAnimation() const { initObserver } = useScrollAnimation()
// 初始化 MarkdownIt 实例
const md = new MarkdownIt({ const md = new MarkdownIt({
html: true, // 允许 HTML 标签 html: true,
linkify: true, // 自动转换 URL 为链接 linkify: true,
typographer: true, // 优化排版 typographer: true,
}) })
// 自定义 heading 渲染规则:自动添加 ID
md.renderer.rules.heading_open = function (tokens, idx) { md.renderer.rules.heading_open = function (tokens, idx) {
const token = tokens[idx] const token = tokens[idx]
const contentToken = tokens[idx + 1] const contentToken = tokens[idx + 1]
const title = contentToken ? contentToken.content : '' const title = contentToken ? contentToken.content : ''
const slug = 'heading-' + title.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-') + '-' + idx const slug = 'heading-' + title.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-') + '-' + idx
return `<${token.tag} id="${slug}">` return `<${token.tag} id="${slug}">`
} }
// 自定义 fence 渲染规则 // 核心修复部分:自定义 fence 渲染规则
md.renderer.rules.fence = function (tokens, idx) { md.renderer.rules.fence = function (tokens, idx) {
const token = tokens[idx] const token = tokens[idx]
const info = token.info ? md.utils.unescapeAll(token.info).trim() : '' const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
@@ -398,36 +381,36 @@ md.renderer.rules.fence = function (tokens, idx) {
const lines = token.content.trimEnd().split('\n') const lines = token.content.trimEnd().split('\n')
const lineNumbers = lines.map((_, i) => i + 1).join('\n') const lineNumbers = lines.map((_, i) => i + 1).join('\n')
// SVG Icon 手动嵌入,不使用 Icon 组件
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`
return ` return `
<div class="ios-code-container my-10 rounded-xl overflow-hidden shadow-2xl bg-[#1e1e1e] border border-white/5 relative group/code"> <div class="ios-code-container my-6 md:my-10 rounded-lg md:rounded-xl overflow-hidden shadow-2xl bg-[#1e1e1e] border border-white/5 relative group/code text-sm">
<!-- Window Header --> <div class="mac-window-header flex items-center justify-between px-3 md:px-4 py-2 md:py-3 bg-[#282828] border-b border-white/5">
<div class="mac-window-header flex items-center justify-between px-4 py-3 bg-[#282828] border-b border-white/5"> <div class="flex items-center gap-3 md:gap-4">
<div class="flex items-center gap-4"> <div class="flex gap-1.5 md:gap-2">
<div class="flex gap-2"> <div class="w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-[#ff5f56]"></div>
<div class="w-3 h-3 rounded-full bg-[#ff5f56]"></div> <div class="w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-[#ffbd2e]"></div>
<div class="w-3 h-3 rounded-full bg-[#ffbd2e]"></div> <div class="w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-[#27c93f]"></div>
<div class="w-3 h-3 rounded-full bg-[#27c93f]"></div>
</div> </div>
<div class="text-xs text-white/30 font-mono select-none uppercase tracking-wider">${langName || 'TEXT'}</div> <div class="text-[10px] md:text-xs text-white/30 font-mono select-none uppercase tracking-wider">${langName || 'TEXT'}</div>
</div> </div>
<!-- Copy Button --> <!-- 修复: 在移动端始终显示(opacity-100),桌面端 Hover 显示 -->
<button class="copy-btn flex items-center gap-1.5 px-2 py-1 rounded hover:bg-white/10 transition-all cursor-pointer opacity-0 group-hover/code:opacity-100" aria-label="Copy code"> <button class="copy-btn flex items-center gap-1.5 px-2 py-1 rounded hover:bg-white/10 transition-all cursor-pointer opacity-100 md:opacity-0 md:group-hover/code:opacity-100" aria-label="Copy code">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-white/40"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg> <span class="text-white/40">${copyIconSvg}</span>
<span class="text-xs text-white/40 font-medium">复制</span> <span class="text-[10px] md:text-xs text-white/40 font-medium">复制</span>
</button> </button>
</div> </div>
<!-- Code Body -->
<div class="code-content-wrapper flex"> <div class="code-content-wrapper flex">
<!-- Line Numbers --> <div class="line-numbers-wrapper hidden md:block select-none text-right border-r border-white/5 bg-[#1e1e1e] py-3 md:py-4 px-2 md:px-3 min-w-[2.5rem] md:min-w-[3rem] text-white/20">
<div class="line-numbers-wrapper select-none text-right border-r border-white/5 bg-[#1e1e1e] py-4 px-3 min-w-[3rem] text-white/20"> <pre class="font-mono text-xs md:text-sm leading-relaxed" style="margin:0;">${lineNumbers}</pre>
<pre class="font-mono text-sm leading-relaxed" style="margin:0;">${lineNumbers}</pre>
</div> </div>
<!-- Code Content --> <!-- 修复: 添加 whitespace-pre 到 code 标签,确保不换行 -->
<div class="code-block-wrapper flex-1 overflow-x-auto py-4 px-4 bg-[#1e1e1e]"> <div class="code-block-wrapper flex-1 overflow-x-auto py-3 md:py-4 px-3 md:px-4 bg-[#1e1e1e] min-w-0 w-full">
<pre class="hljs !bg-transparent !m-0 !p-0 !rounded-none"><code class="!font-mono text-sm leading-relaxed block">${highlighted}</code></pre> <pre class="hljs !bg-transparent !m-0 !p-0 !rounded-none !overflow-visible"><code class="!font-mono text-xs md:text-sm leading-relaxed block !whitespace-pre">${highlighted}</code></pre>
</div> </div>
</div> </div>
</div> </div>
@@ -441,10 +424,8 @@ const renderedContent = computed(() => {
const generateToc = () => { const generateToc = () => {
if (!articleRef.value) return if (!articleRef.value) return
const headers = articleRef.value.querySelectorAll('h2, h3') const headers = articleRef.value.querySelectorAll('h2, h3')
const newToc: TocItem[] = [] const newToc: TocItem[] = []
headers.forEach((header) => { headers.forEach((header) => {
if (header.id && header.textContent) { if (header.id && header.textContent) {
newToc.push({ newToc.push({
@@ -454,30 +435,23 @@ const generateToc = () => {
}) })
} }
}) })
toc.value = newToc toc.value = newToc
} }
// 滚动监听逻辑
const initScrollSpy = () => { const initScrollSpy = () => {
if (!articleRef.value) return if (!articleRef.value) return
const headers = articleRef.value.querySelectorAll('h2, h3') const headers = articleRef.value.querySelectorAll('h2, h3')
if (headers.length === 0) return if (headers.length === 0) return
if (observer.value) observer.value.disconnect() if (observer.value) observer.value.disconnect()
observer.value = new IntersectionObserver((entries) => { observer.value = new IntersectionObserver((entries) => {
// 核心修复 2: 如果是点击目录触发的滚动,不更新 activeId防止抽搐
if (isClickScrolling.value) return if (isClickScrolling.value) return
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
activeId.value = entry.target.id activeId.value = entry.target.id
} }
}) })
}, { }, {
// 微调 rootMargin只检测顶部区域
rootMargin: '-100px 0px -80% 0px' rootMargin: '-100px 0px -80% 0px'
}) })
@@ -486,29 +460,21 @@ const initScrollSpy = () => {
}) })
} }
// 点击滚动到标题
const scrollToHeading = (id: string) => { const scrollToHeading = (id: string) => {
const element = document.getElementById(id) const element = document.getElementById(id)
if (element) { if (element) {
// 核心修复 3: 开启点击滚动锁,并立即设置高亮
isClickScrolling.value = true isClickScrolling.value = true
activeId.value = id activeId.value = id
// 清除可能存在的旧定时器
if (scrollTimeout) clearTimeout(scrollTimeout) if (scrollTimeout) clearTimeout(scrollTimeout)
const offset = 100 const offset = 100
const bodyRect = document.body.getBoundingClientRect().top const bodyRect = document.body.getBoundingClientRect().top
const elementRect = element.getBoundingClientRect().top const elementRect = element.getBoundingClientRect().top
const elementPosition = elementRect - bodyRect const elementPosition = elementRect - bodyRect
const offsetPosition = elementPosition - offset const offsetPosition = elementPosition - offset
window.scrollTo({ window.scrollTo({
top: offsetPosition, top: offsetPosition,
behavior: 'smooth' behavior: 'smooth'
}) })
// 1秒后解锁此时平滑滚动应该已经结束
scrollTimeout = window.setTimeout(() => { scrollTimeout = window.setTimeout(() => {
isClickScrolling.value = false isClickScrolling.value = false
}, 1000) }, 1000)
@@ -540,7 +506,8 @@ const addCopyListeners = () => {
const text = codeElement.innerText const text = codeElement.innerText
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
const originalContent = newBtn.innerHTML const originalContent = newBtn.innerHTML
newBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-400"><polyline points="20 6 9 17 4 12"/></svg><span class="text-green-400 text-xs font-medium">已复制</span>` const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-400"><polyline points="20 6 9 17 4 12"/></svg>`
newBtn.innerHTML = `<span>${checkIconSvg}</span><span class="text-green-400 text-[10px] md:text-xs font-medium">已复制</span>`
setTimeout(() => { setTimeout(() => {
newBtn.innerHTML = originalContent newBtn.innerHTML = originalContent
}, 2000) }, 2000)
@@ -621,7 +588,6 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
/* 隐藏滚动条但保留滚动功能 */
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
@@ -630,7 +596,6 @@ onBeforeUnmount(() => {
scrollbar-width: none; scrollbar-width: none;
} }
/* Tailwind Typography (:prose) 覆盖 */
:deep(.prose pre) { :deep(.prose pre) {
background-color: transparent !important; background-color: transparent !important;
margin: 0 !important; margin: 0 !important;
@@ -638,10 +603,24 @@ onBeforeUnmount(() => {
border-radius: 0 !important; border-radius: 0 !important;
box-shadow: none !important; box-shadow: none !important;
border: none !important; border: none !important;
white-space: pre !important; /* 关键:强制不自动换行 */
} }
:deep(.prose code) { :deep(.prose code) {
font-family: 'SF Mono', Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: 'SF Mono', Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
white-space: pre !important; /* 关键:强制不自动换行 */
}
/* 覆盖 highlight.js 或其他样式可能带来的换行 */
:deep(.code-block-wrapper pre code) {
white-space: pre !important;
word-wrap: normal !important;
overflow-wrap: normal !important;
}
/* 允许容器横向滚动 */
:deep(.code-block-wrapper) {
-webkit-overflow-scrolling: touch; /* iOS 惯性滚动 */
} }
:deep(.prose :not(pre) > code) { :deep(.prose :not(pre) > code) {
@@ -650,33 +629,54 @@ onBeforeUnmount(() => {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
border-radius: 0.3em; border-radius: 0.3em;
font-weight: 500; font-weight: 500;
word-break: break-word; /* 内联代码允许换行,防止撑开 */
} }
:deep(.prose h1), :deep(.prose h2), :deep(.prose h3) { :deep(.prose h1), :deep(.prose h2), :deep(.prose h3) {
color: #fff; color: #fff;
font-family: serif; font-family: serif;
margin-top: 2em; margin-top: 1.5em;
margin-bottom: 0.8em; margin-bottom: 0.6em;
line-height: 1.3; line-height: 1.3;
scroll-margin-top: 120px; scroll-margin-top: 100px;
}
@media (min-width: 768px) {
:deep(.prose h1), :deep(.prose h2), :deep(.prose h3) {
margin-top: 2em;
margin-bottom: 0.8em;
scroll-margin-top: 120px;
}
} }
:deep(.prose h2) { :deep(.prose h2) {
font-size: 1.8rem; font-size: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 0.5rem; padding-bottom: 0.3rem;
}
@media (min-width: 768px) {
:deep(.prose h2) {
font-size: 1.8rem;
padding-bottom: 0.5rem;
}
} }
:deep(.prose h3) { :deep(.prose h3) {
font-size: 1.4rem; font-size: 1.25rem;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
@media (min-width: 768px) {
:deep(.prose h3) {
font-size: 1.4rem;
}
}
:deep(.prose a) { :deep(.prose a) {
color: #d4b383; color: #d4b383;
text-decoration: none; text-decoration: none;
border-bottom: 1px dashed rgba(212, 179, 131, 0.5); border-bottom: 1px dashed rgba(212, 179, 131, 0.5);
transition: all 0.2s; transition: all 0.2s;
word-break: break-all;
} }
:deep(.prose a:hover) { :deep(.prose a:hover) {
@@ -687,20 +687,36 @@ onBeforeUnmount(() => {
:deep(.prose blockquote) { :deep(.prose blockquote) {
border-left: 3px solid #d4b383; border-left: 3px solid #d4b383;
background: rgba(255, 255, 255, 0.02); background: rgba(255, 255, 255, 0.02);
padding: 1rem 1.5rem; padding: 0.8rem 1rem;
border-radius: 0 0.5rem 0.5rem 0; border-radius: 0 0.5rem 0.5rem 0;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
font-style: italic; font-style: italic;
} }
@media (min-width: 768px) {
:deep(.prose blockquote) {
padding: 1rem 1.5rem;
}
}
:deep(.prose ul li::marker) { :deep(.prose ul li::marker) {
color: #d4b383; color: #d4b383;
} }
:deep(.prose img) {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
}
:deep(.code-block-wrapper::-webkit-scrollbar) { :deep(.code-block-wrapper::-webkit-scrollbar) {
height: 8px; height: 6px;
background-color: transparent; background-color: transparent;
} }
@media (min-width: 768px) {
:deep(.code-block-wrapper::-webkit-scrollbar) {
height: 8px;
}
}
:deep(.code-block-wrapper::-webkit-scrollbar-thumb) { :deep(.code-block-wrapper::-webkit-scrollbar-thumb) {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
@@ -719,7 +735,6 @@ onBeforeUnmount(() => {
background: transparent !important; background: transparent !important;
} }
/* 移动端动画 */
@keyframes slide-up { @keyframes slide-up {
from { from {
transform: translateY(100%); transform: translateY(100%);