数据结构优化

This commit is contained in:
李琦
2026-01-20 16:45:23 +08:00
parent 8460820f58
commit a941e46a0d
13 changed files with 401 additions and 247 deletions

View File

@@ -2,15 +2,9 @@
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/icon/icon.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>艺术与代码 | 年糕崽崽的数字花园</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Sans+SC:wght@300;400;500;700&family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
<div id="app"></div>

View File

@@ -9,10 +9,15 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource/inter": "^5.0.0",
"@fontsource/jetbrains-mono": "^5.0.0",
"@fontsource/noto-sans-sc": "^5.0.0",
"@fontsource/playfair-display": "^5.0.0",
"@types/markdown-it": "^14.1.2",
"echarts": "^6.0.0",
"highlight": "^0.2.4",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^0.446.0",
"markdown-it": "^14.1.0",
"md-editor-v3": "^6.3.1",
"vue": "^3.5.13",

44
client/pnpm-lock.yaml generated
View File

@@ -8,6 +8,18 @@ importers:
.:
dependencies:
'@fontsource/inter':
specifier: ^5.0.0
version: 5.2.8
'@fontsource/jetbrains-mono':
specifier: ^5.0.0
version: 5.2.8
'@fontsource/noto-sans-sc':
specifier: ^5.0.0
version: 5.2.9
'@fontsource/playfair-display':
specifier: ^5.0.0
version: 5.2.8
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
@@ -20,6 +32,9 @@ importers:
highlight.js:
specifier: ^11.11.1
version: 11.11.1
lucide-vue-next:
specifier: ^0.446.0
version: 0.446.0(vue@3.5.26(typescript@5.6.3))
markdown-it:
specifier: ^14.1.0
version: 14.1.0
@@ -327,6 +342,18 @@ packages:
cpu: [x64]
os: [win32]
'@fontsource/inter@5.2.8':
resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==}
'@fontsource/jetbrains-mono@5.2.8':
resolution: {integrity: sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==}
'@fontsource/noto-sans-sc@5.2.9':
resolution: {integrity: sha512-bTUIWGBgJDpwi5qAr+x0/lcgv80IHTB9vl6s2f6EymZEa7qYV99yNRBZuKFT+SYDKVunZrjCEhWtpxqmbXWl5Q==}
'@fontsource/playfair-display@5.2.8':
resolution: {integrity: sha512-fUEhib70SszNhQVsGbUMSsWJhr7Je0rdeuZdtGpDNu0GKF1xJM8QhpI/y0pckU25GcChXm9TLOmeZupkvvZo2g==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -817,6 +844,11 @@ packages:
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
engines: {node: 20 || >=22}
lucide-vue-next@0.446.0:
resolution: {integrity: sha512-ddx9NRczKulV4a2GcbY9F5l/zuUW0ARB4mRc+kOzFur9XvvrVxVmAmhqVChZUlj9Wmraopn8ujDfhxB6FlVfIw==}
peerDependencies:
vue: '>=3.0.1'
lucide-vue-next@0.543.0:
resolution: {integrity: sha512-Az5kpNm/koKAwSNIKjsZ4uHV2tVfmlQlcHwFBygQ8gc5/jFg7An9OrxgDy/aE5m+HLx7VfLYqDxLr8gWecZbQA==}
peerDependencies:
@@ -1474,6 +1506,14 @@ snapshots:
'@esbuild/win32-x64@0.25.12':
optional: true
'@fontsource/inter@5.2.8': {}
'@fontsource/jetbrains-mono@5.2.8': {}
'@fontsource/noto-sans-sc@5.2.9': {}
'@fontsource/playfair-display@5.2.8': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1981,6 +2021,10 @@ snapshots:
lru-cache@11.2.4: {}
lucide-vue-next@0.446.0(vue@3.5.26(typescript@5.6.3)):
dependencies:
vue: 3.5.26(typescript@5.6.3)
lucide-vue-next@0.543.0(vue@3.5.26(typescript@5.6.3)):
dependencies:
vue: 3.5.26(typescript@5.6.3)

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
client/public/icon/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,25 +1,28 @@
<template>
<div class="min-h-screen bg-art-bg text-art-text relative overflow-hidden">
<!--
CRITICAL FIX: Removed 'overflow-hidden' from this root div.
'overflow-hidden' on an ancestor element breaks 'position: sticky'
in child components (like BlogDetail TOC) because it changes the
scrolling context.
-->
<div class="min-h-screen bg-art-bg text-art-text relative">
<!-- Star Background (Only for frontend) -->
<!-- Removed the gradient background div to keep original theme color -->
<StarBackground v-if="!isAdminOrLogin" class="z-0" />
<!-- Global Noise (Only for frontend) -->
<!-- z-index 提高到 60 保持在最上层 -->
<div v-if="!isAdminOrLogin" class="fixed inset-0 pointer-events-none z-[60] bg-noise opacity-30 mix-blend-overlay"></div>
<!-- Toast Container -->
<div id="toast-container" class="toast-container relative z-[70]"></div>
<!-- Header (Hide on Admin & Login) -->
<Header v-if="!isAdminOrLogin" class="z-50" />
<Header v-if="!isAdminOrLogin" class="relative z-50" />
<!-- Mobile Menu (Hide on Admin & Login) -->
<MobileMenu v-if="!isAdminOrLogin" class="relative z-50" />
<!-- Main Content -->
<!-- Conditional classes: Apply max-w-7xl only for frontend pages -->
<!-- 添加 relative z-10 确保内容浮在星星之上 -->
<main :class="[
isAdminOrLogin ? 'w-full h-full' : 'pt-32 pb-20 px-6 max-w-7xl mx-auto relative z-10'
]">
@@ -44,7 +47,7 @@ import Header from './components/Header.vue'
import MobileMenu from './components/MobileMenu.vue'
import Footer from './components/Footer.vue'
import InquiryModal from './components/InquiryModal.vue'
import StarBackground from './components/StarBackground.vue' // Import the new component
import StarBackground from './components/StarBackground.vue'
import { getPublicSettings } from './services/api'
const route = useRoute()
@@ -55,11 +58,16 @@ const isAdminOrLogin = computed(() => {
return path.startsWith('/admin') || path === '/login'
})
// 应用网站配置到meta标签(页面标题由路由守卫处理)
// 应用网站配置到页面标题和meta标签
const applySiteSettings = async () => {
try {
const settings = await getPublicSettings()
// 设置页面标题
if (settings.site_title) {
document.title = settings.site_title
}
// 更新meta标签
const updateMetaTag = (name: string, content: string) => {
if (!content) return

View File

@@ -62,9 +62,9 @@
</button>
</nav>
<div class="hidden md:flex items-center gap-4">
<a href="#" class="text-art-muted hover:text-white transition-colors"><i data-lucide="github" class="w-5 h-5"></i></a>
<a href="#" class="text-art-muted hover:text-white transition-colors"><Icon name="github" :size="20" /></a>
<a href="/admin" target="_blank" class="text-art-muted hover:text-white transition-colors" title="后台管理">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
<Icon name="layout-dashboard" :size="20" />
</a>
<button
v-if="visibleMenus.includes('services')"
@@ -75,7 +75,7 @@
</button>
</div>
<button class="md:hidden text-white" @click="toggleMobileMenu">
<i data-lucide="menu" class="w-6 h-6"></i>
<Icon name="menu" :size="24" />
</button>
</div>
</header>
@@ -85,6 +85,7 @@
import { ref, onMounted, onUpdated, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { getPublicSettings } from '../services/api'
import Icon from './Icon.vue'
const router = useRouter()
const route = useRoute()
@@ -122,11 +123,7 @@ const updateActiveNav = () => {
else if (path === '/about') activeNav.value = 'about'
}
const refreshIcons = () => {
if ((window as any).lucide) {
(window as any).lucide.createIcons()
}
}
// Icons are now handled by Vue components
// 获取网站配置
const loadSiteSettings = async () => {
@@ -157,19 +154,13 @@ const loadSiteSettings = async () => {
onMounted(() => {
updateActiveNav()
refreshIcons()
loadSiteSettings()
})
onUpdated(() => {
refreshIcons()
})
watch(
() => route.path,
() => {
updateActiveNav()
nextTick(refreshIcons)
}
)

View File

@@ -1,40 +1,38 @@
<template>
<i
:data-lucide="name"
<component
:is="iconComponent"
:class="className"
:style="style"
ref="iconRef"
></i>
:size="size"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { computed } from 'vue'
import * as LucideIcons from 'lucide-vue-next'
interface Props {
name: string
className?: string
style?: Record<string, any>
size?: number | string
}
const props = withDefaults(defineProps<Props>(), {
className: '',
style: () => ({})
style: () => ({}),
size: 24
})
const iconRef = ref<HTMLElement | null>(null)
const updateIcon = () => {
if (window.lucide && iconRef.value) {
// 重新创建图标
window.lucide.createIcons()
}
// 将kebab-case转换为PascalCase例如 arrow-left -> ArrowLeft
const toPascalCase = (str: string) => {
return str.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join('')
}
onMounted(() => {
updateIcon()
})
watch(() => props.name, () => {
updateIcon()
const iconComponent = computed(() => {
const iconName = toPascalCase(props.name) as keyof typeof LucideIcons
return LucideIcons[iconName] || LucideIcons.AlertCircle
})
</script>

View File

@@ -4,6 +4,21 @@ import "./style.css"
import "highlight.js/styles/atom-one-dark.css"
import router from "./router"
// 导入字体
import "@fontsource/inter/300.css"
import "@fontsource/inter/400.css"
import "@fontsource/inter/500.css"
import "@fontsource/inter/600.css"
import "@fontsource/noto-sans-sc/300.css"
import "@fontsource/noto-sans-sc/400.css"
import "@fontsource/noto-sans-sc/500.css"
import "@fontsource/noto-sans-sc/700.css"
import "@fontsource/playfair-display/400.css"
import "@fontsource/playfair-display/400-italic.css"
import "@fontsource/playfair-display/600.css"
import "@fontsource/playfair-display/700.css"
import "@fontsource/jetbrains-mono/400.css"
const app = createApp(App)
app.use(router)
app.mount("#app")

View File

@@ -1,12 +1,12 @@
<template>
<section id="blog-detail" class="page-section block animate-slide-down">
<div class="max-w-7xl mx-auto pt-32 px-6 pb-20 relative">
<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">
<!-- 返回按钮 -->
<button
@click="$router.push('/blog')"
class="group flex items-center gap-2 text-art-muted hover:text-white transition-colors mb-12"
class="group flex items-center gap-2 text-art-muted hover:text-white transition-colors mb-8 md:mb-12"
>
<i data-lucide="arrow-left" class="w-4 h-4 transform group-hover:-translate-x-1 transition-transform"></i>
<Icon name="arrow-left" :size="16" class="transform group-hover:-translate-x-1 transition-transform" />
<span class="text-sm font-medium tracking-wide">返回文章列表</span>
</button>
@@ -26,19 +26,86 @@
</button>
</div>
<!-- Blog detail content -->
<div v-else class="xl:flex xl:gap-12 relative">
<!-- 主内容区域 -->
<div class="flex-1 space-y-12">
<!-- 文章信息 -->
<div class="border-b border-white/5 pb-10">
<div class="flex items-center gap-4 mb-6">
<!-- Blog Layout -->
<div v-else class="flex flex-col xl:flex-row xl:gap-10 items-start relative">
<!-- 1. 左侧边栏元信息 - 仅在大屏显示 -->
<aside class="hidden xl:block w-64 flex-shrink-0 order-1 sticky top-32 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar">
<div class="space-y-6">
<!-- 专栏跳转卡片 -->
<div v-if="post.columnId && post.columnName" 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-3 flex items-center gap-2">
<Icon name="book-open" :size="14" class="text-art-accent" />
所属专栏
</h3>
<a
@click.prevent="router.push(`/columns/${post.columnId}`)"
href="#"
class="block p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-1">
{{ post.columnName }}
</div>
<div class="text-xs text-art-muted flex items-center gap-1">
<Icon name="arrow-right" :size="12" />
查看专栏
</div>
</a>
</div>
<!-- 分类搜索卡片 -->
<div v-if="post.categoryId && post.categoryName" 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-3 flex items-center gap-2">
<Icon name="folder" :size="14" class="text-art-accent" />
分类
</h3>
<a
@click.prevent="router.push(`/blog?category=${post.categoryId}`)"
href="#"
class="block p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-1">
{{ post.categoryName }}
</div>
<div class="text-xs text-art-muted flex items-center gap-1">
<Icon name="arrow-right" :size="12" />
查看分类
</div>
</a>
</div>
<!-- 标签搜索卡片 -->
<div v-if="post.tags && post.tags.length > 0" 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-3 flex items-center gap-2">
<Icon name="tags" :size="14" class="text-art-accent" />
标签
</h3>
<div class="flex flex-wrap gap-2">
<a
v-for="tag in post.tags"
:key="tag.id"
@click.prevent="router.push(`/blog?tag=${tag.id}`)"
href="#"
class="px-2.5 py-1 rounded-md bg-white/5 border border-white/10 hover:bg-art-accent/20 hover:border-art-accent/50 hover:text-art-accent text-xs text-art-muted transition-all duration-200"
>
{{ tag.name }}
</a>
</div>
</div>
</div>
</aside>
<!-- 2. 中间文章主内容 -->
<div class="flex-1 min-w-0 order-2">
<!-- 文章头图与信息 -->
<div class="border-b border-white/5 pb-8 mb-10">
<div class="flex flex-wrap items-center gap-3 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="text-sm text-art-muted">{{ post.date }}</span>
</div>
<h1 class="font-serif text-4xl md:text-5xl lg:text-6xl text-white leading-tight mb-8">{{ post.title }}</h1>
<h1 class="font-serif text-3xl md:text-5xl lg:text-6xl text-white leading-tight mb-8 break-words">{{ post.title }}</h1>
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-full overflow-hidden border border-white/20">
<div class="w-10 h-10 rounded-full overflow-hidden border border-white/20 flex-shrink-0">
<img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=NianGao"
alt="Avatar"
@@ -52,148 +119,180 @@
</div>
</div>
<!-- 文章内容 - 使用 v-html 渲染 Markdown 解析后的 HTML -->
<!-- ref="articleRef" 用于后续绑定复制事件和提取目录 -->
<article ref="articleRef" class="blog-content prose prose-invert prose-lg max-w-none text-art-muted leading-relaxed">
<!-- 文章内容 -->
<article
ref="articleRef"
class="blog-content prose prose-invert max-w-none text-art-muted leading-relaxed prose-base md:prose-lg"
>
<div v-html="renderedContent"></div>
</article>
</div>
<!-- 目录侧边栏 (仅在 XL 屏幕显示) -->
<aside class="hidden xl:block w-64 flex-shrink-0">
<div class="sticky top-32 space-y-6">
<!-- 目录导航 -->
<div class="p-6 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-xl transition-all duration-300 hover:bg-white/10">
<h3 class="text-sm font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<i data-lucide="list" class="w-4 h-4 text-art-accent"></i>
目录导航
</h3>
<nav class="space-y-1 relative">
<!-- 激活状态的指示条 -->
<div
class="absolute left-0 w-0.5 bg-art-accent transition-all duration-300 ease-out"
:style="activeIndicatorStyle"
v-if="toc.length > 0"
></div>
<ul class="space-y-1 border-l border-white/10 ml-[1px]">
<li v-for="item in toc" :key="item.id">
<a
@click.prevent="scrollToHeading(item.id)"
href="#"
class="block text-xs py-1.5 transition-all duration-200 border-l-2 border-transparent pl-4 hover:text-white"
:class="[
activeId === item.id ? 'text-art-accent font-medium' : 'text-art-muted',
item.level > 2 ? 'pl-8' : 'pl-4'
]"
>
{{ item.text }}
</a>
</li>
</ul>
<div v-if="toc.length === 0" class="text-xs text-art-muted italic pl-4">
暂无目录
</div>
</nav>
</div>
<!-- 专栏跳转卡片 -->
<div v-if="post.columnId && post.columnName" class="p-6 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-xl transition-all duration-300 hover:bg-white/10">
<h3 class="text-sm font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<i data-lucide="book-open" class="w-4 h-4 text-art-accent"></i>
所属专栏
</h3>
<a
@click.prevent="router.push(`/columns/${post.columnId}`)"
href="#"
class="block p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-1">
{{ post.columnName }}
</div>
<div class="text-xs text-art-muted flex items-center gap-1">
<i data-lucide="arrow-right" class="w-3 h-3"></i>
查看专栏
</div>
</a>
</div>
<!-- 分类搜索卡片 -->
<div v-if="post.categoryId && post.categoryName" class="p-6 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-xl transition-all duration-300 hover:bg-white/10">
<h3 class="text-sm font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<i data-lucide="folder" class="w-4 h-4 text-art-accent"></i>
分类
</h3>
<a
@click.prevent="router.push(`/blog?category=${post.categoryId}`)"
href="#"
class="block p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-1">
{{ post.categoryName }}
</div>
<div class="text-xs text-art-muted flex items-center gap-1">
<i data-lucide="arrow-right" class="w-3 h-3"></i>
查看分类
</div>
</a>
</div>
<!-- 标签搜索卡片 -->
<div v-if="post.tags && post.tags.length > 0" class="p-6 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-xl transition-all duration-300 hover:bg-white/10">
<h3 class="text-sm font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<i data-lucide="tags" class="w-4 h-4 text-art-accent"></i>
<!-- 移动端专属信息补充区 -->
<div class="xl:hidden mt-12 border-t border-white/10 pt-8 space-y-6">
<!-- 标签补充 -->
<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">
<Icon name="tags" :size="14" class="text-art-accent" />
标签
</h3>
</h4>
<div class="flex flex-wrap gap-2">
<a
v-for="tag in post.tags"
:key="tag.id"
@click.prevent="router.push(`/blog?tag=${tag.id}`)"
href="#"
class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 hover:bg-art-accent/20 hover:border-art-accent/50 hover:text-art-accent text-xs text-art-muted transition-all duration-200"
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"
>
{{ tag.name }}
# {{ tag.name }}
</a>
</div>
</div>
<!-- 推荐博客 -->
<div class="p-6 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-xl transition-all duration-300 hover:bg-white/10">
<h3 class="text-sm font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<i data-lucide="sparkles" class="w-4 h-4 text-art-accent"></i>
推荐阅读
</h3>
<div v-if="loadingRecommendations" class="flex justify-center py-4">
<div class="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-art-accent"></div>
<!-- 专栏/分类补充 -->
<div class="flex flex-wrap gap-4">
<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" />
专栏: <span class="text-white">{{ post.columnName }}</span>
</div>
<div v-else-if="recommendedPosts.length === 0" class="text-xs text-art-muted italic py-4">
暂无推荐
<div v-if="post.categoryName" class="flex items-center gap-2 text-sm text-art-muted">
<Icon name="folder" :size="14" class="text-art-accent" />
分类: <span class="text-white">{{ post.categoryName }}</span>
</div>
<div v-else class="space-y-3">
</div>
</div>
<!-- 底部推荐阅读 -->
<div class="mt-12 md:mt-20 border-t border-white/10 pt-10">
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-2">
<Icon name="sparkles" :size="20" class="text-art-accent" />
推荐阅读
</h3>
<div v-if="loadingRecommendations" class="flex justify-center py-4">
<div class="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-art-accent"></div>
</div>
<div v-else-if="recommendedPosts.length === 0" class="text-sm text-art-muted italic">
暂无推荐
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
<router-link
v-for="recPost in recommendedPosts"
:key="recPost.id"
:to="`/blog/${recPost.id}`"
custom
v-slot="{ href, navigate }"
>
<a
v-for="recPost in recommendedPosts"
:key="recPost.id"
@click.prevent="router.push(`/blog/${recPost.id}`)"
href="#"
class="block p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group"
:href="href"
target="_blank"
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"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-2 line-clamp-2">
<div class="text-base font-medium text-white group-hover:text-art-accent transition-colors mb-3 line-clamp-2">
{{ recPost.title }}
</div>
<div class="flex items-center gap-3 text-xs text-art-muted">
<div class="flex items-center justify-between text-xs text-art-muted mt-auto">
<span v-if="recPost.categoryName" class="px-2 py-0.5 rounded border border-white/20">
{{ recPost.categoryName }}
</span>
<span>{{ recPost.date }}</span>
</div>
</a>
</div>
</router-link>
</div>
</div>
</div>
<!-- 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">
<!-- 目录导航 -->
<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">
<Icon name="list" :size="14" class="text-art-accent" />
目录导航
</h3>
<nav class="space-y-1 relative">
<!-- 激活状态的指示条 -->
<div
class="absolute left-0 w-0.5 bg-art-accent transition-all duration-300 ease-out"
:style="activeIndicatorStyle"
v-if="toc.length > 0"
></div>
<ul class="space-y-1 border-l border-white/10 ml-[1px]">
<li v-for="item in toc" :key="item.id">
<a
@click.prevent="scrollToHeading(item.id)"
href="#"
class="block text-xs py-1.5 transition-all duration-200 border-l-2 border-transparent pl-4 hover:text-white truncate"
:class="[
activeId === item.id ? 'text-art-accent font-medium' : 'text-art-muted',
item.level > 2 ? 'pl-8' : 'pl-4'
]"
:title="item.text"
>
{{ item.text }}
</a>
</li>
</ul>
<div v-if="toc.length === 0" class="text-xs text-art-muted italic pl-4">
暂无目录
</div>
</nav>
</div>
</aside>
</div>
</div>
<!-- Mobile TOC Floating Button -->
<button
v-if="toc.length > 0 && !loading && !error"
@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"
aria-label="目录"
>
<Icon name="list" :size="20" />
</button>
<!-- Mobile TOC Drawer -->
<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>
<!-- 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="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">
<Icon name="list" :size="16" class="text-art-accent" />
文章目录
</h3>
<button @click="showMobileToc = false" class="text-art-muted hover:text-white p-2">
<Icon name="x" :size="20" />
</button>
</div>
<div class="overflow-y-auto p-4 flex-1">
<nav class="space-y-1">
<ul class="space-y-2">
<li v-for="item in toc" :key="item.id">
<a
@click.prevent="scrollToHeading(item.id); showMobileToc = false"
href="#"
class="block text-sm py-1 transition-all duration-200 hover:text-white"
:class="[
activeId === item.id ? 'text-art-accent font-medium' : 'text-art-muted',
item.level > 2 ? 'pl-4' : ''
]"
>
{{ item.text }}
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</section>
</template>
@@ -202,6 +301,7 @@ import { ref, onMounted, computed, nextTick, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchPost, getRecommendedPosts, Post, getPublicSettings } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import Icon from '../components/Icon.vue'
// 引入 Markdown 解析依赖
import MarkdownIt from 'markdown-it'
@@ -210,10 +310,7 @@ import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
const route = useRoute()
// Initialize router for template use ($router.push)
const router = useRouter()
// Mark router as used (it's used in template via $router)
void router
const postId = route.params.id as string
const articleRef = ref<HTMLElement | null>(null)
@@ -226,6 +323,10 @@ interface TocItem {
const toc = ref<TocItem[]>([])
const activeId = ref<string>('')
const observer = ref<IntersectionObserver | null>(null)
const showMobileToc = ref(false)
// 核心修复 1: 引入点击滚动标志位
const isClickScrolling = ref(false)
let scrollTimeout: number | null = null
const post = ref<Post>({
id: Number(postId) || 0,
@@ -256,14 +357,12 @@ md.renderer.rules.heading_open = function (tokens, idx) {
const contentToken = tokens[idx + 1]
const title = contentToken ? contentToken.content : ''
// 生成简单的 slug ID (移除特殊字符,转小写)
// 注意:这只是一个简单的处理,实际生产中可能需要更严谨的 slugify 库
const slug = 'heading-' + title.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-') + '-' + idx
return `<${token.tag} id="${slug}">`
}
// 自定义 fence 渲染规则以实现 iOS 风格代码块 + 行号 + 复制功能
// 自定义 fence 渲染规则
md.renderer.rules.fence = function (tokens, idx) {
const token = tokens[idx]
const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
@@ -274,7 +373,6 @@ md.renderer.rules.fence = function (tokens, idx) {
langName = info.split(/\s+/g)[0]
}
// 1. 代码高亮处理
if (langName && hljs.getLanguage(langName)) {
try {
highlighted = hljs.highlight(token.content, { language: langName, ignoreIllegals: true }).value
@@ -285,11 +383,9 @@ md.renderer.rules.fence = function (tokens, idx) {
highlighted = md.utils.escapeHtml(token.content)
}
// 2. 生成行号
const lines = token.content.trimEnd().split('\n')
const lineNumbers = lines.map((_, i) => i + 1).join('\n')
// 3. 构建 iOS 风格 HTML 结构
return `
<div class="ios-code-container my-10 rounded-xl overflow-hidden shadow-2xl bg-[#1e1e1e] border border-white/5 relative group/code">
<!-- Window Header -->
@@ -326,17 +422,14 @@ md.renderer.rules.fence = function (tokens, idx) {
`
}
// 计算属性:将 Markdown 内容转换为 HTML
const renderedContent = computed(() => {
if (!post.value.content) return ''
return md.render(post.value.content)
})
// 生成目录结构
const generateToc = () => {
if (!articleRef.value) return
// 查找所有 h2, h3 标签
const headers = articleRef.value.querySelectorAll('h2, h3')
const newToc: TocItem[] = []
@@ -360,18 +453,21 @@ const initScrollSpy = () => {
const headers = articleRef.value.querySelectorAll('h2, h3')
if (headers.length === 0) return
// 断开旧的观察者
if (observer.value) observer.value.disconnect()
// 创建新的观察者
// rootMargin: '-10% 0px -80% 0px' 意味着当标题进入视口顶部 10% 到 20% 的区域时触发
observer.value = new IntersectionObserver((entries) => {
// 核心修复 2: 如果是点击目录触发的滚动,不更新 activeId防止抽搐
if (isClickScrolling.value) return
entries.forEach(entry => {
if (entry.isIntersecting) {
activeId.value = entry.target.id
}
})
}, { rootMargin: '-80px 0px -80% 0px' })
}, {
// 微调 rootMargin只检测顶部区域
rootMargin: '-100px 0px -80% 0px'
})
headers.forEach(header => {
observer.value?.observe(header)
@@ -382,7 +478,13 @@ const initScrollSpy = () => {
const scrollToHeading = (id: string) => {
const element = document.getElementById(id)
if (element) {
// 减去顶部导航栏的高度偏移 (比如 100px)
// 核心修复 3: 开启点击滚动锁,并立即设置高亮
isClickScrolling.value = true
activeId.value = id
// 清除可能存在的旧定时器
if (scrollTimeout) clearTimeout(scrollTimeout)
const offset = 100
const bodyRect = document.body.getBoundingClientRect().top
const elementRect = element.getBoundingClientRect().top
@@ -394,18 +496,16 @@ const scrollToHeading = (id: string) => {
behavior: 'smooth'
})
// 手动更新 activeId防止滚动动画期间观察者跳动
activeId.value = id
// 1秒后解锁此时平滑滚动应该已经结束
scrollTimeout = window.setTimeout(() => {
isClickScrolling.value = false
}, 1000)
}
}
// 计算指示条样式
const activeIndicatorStyle = computed(() => {
const index = toc.value.findIndex(item => item.id === activeId.value)
if (index === -1) return { top: '0', opacity: 0 }
// 假设每个目录项高度大约是 28px (text-xs + py-1.5 + gap)
// 这里可以根据实际 DOM 计算更精确,简单起见用固定高度估算
const itemHeight = 28
return {
top: `${index * itemHeight}px`,
@@ -414,28 +514,21 @@ const activeIndicatorStyle = computed(() => {
}
})
// 处理复制功能的逻辑
const addCopyListeners = () => {
if (!articleRef.value) return
const buttons = articleRef.value.querySelectorAll('.copy-btn')
buttons.forEach(btn => {
const newBtn = btn.cloneNode(true) as HTMLElement
btn.parentNode?.replaceChild(newBtn, btn)
newBtn.addEventListener('click', async () => {
const container = newBtn.closest('.ios-code-container')
const codeElement = container?.querySelector('code')
if (codeElement) {
try {
const text = codeElement.innerText
await navigator.clipboard.writeText(text)
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>`
setTimeout(() => {
newBtn.innerHTML = originalContent
}, 2000)
@@ -447,7 +540,6 @@ const addCopyListeners = () => {
})
}
// 更新页面标题
const updatePageTitle = async (articleTitle: string) => {
try {
const settings = await getPublicSettings()
@@ -455,7 +547,6 @@ const updatePageTitle = async (articleTitle: string) => {
document.title = `${siteTitle} | ${articleTitle}`
} catch (error) {
console.error('Failed to update page title:', error)
// 即使获取配置失败,也更新标题
document.title = `年糕崽崽.Dev | ${articleTitle}`
}
}
@@ -467,23 +558,16 @@ const fetchBlogDetail = async () => {
const postData = await fetchPost(postId)
if (postData) {
post.value = postData
// 更新页面标题
if (postData.title) {
await updatePageTitle(postData.title)
}
// 获取推荐文章
fetchRecommendations()
// 初始化动画、事件监听和目录 - 在数据加载完成后
nextTick(() => {
setTimeout(() => {
initObserver()
addCopyListeners()
generateToc() // 生成目录
initScrollSpy() // 启动滚动监听
if (window.lucide) window.lucide.createIcons()
generateToc()
initScrollSpy()
}, 100)
})
} else {
@@ -497,7 +581,6 @@ const fetchBlogDetail = async () => {
}
}
// 获取推荐文章
const fetchRecommendations = async () => {
loadingRecommendations.value = true
try {
@@ -519,13 +602,23 @@ onBeforeUnmount(() => {
if (observer.value) {
observer.value.disconnect()
}
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
})
</script>
<style scoped>
/* Tailwind Typography (:prose) 覆盖 */
/* 隐藏滚动条但保留滚动功能 */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 移除 prose 默认的 pre 样式,因为我们自定义了容器 */
/* Tailwind Typography (:prose) 覆盖 */
:deep(.prose pre) {
background-color: transparent !important;
margin: 0 !important;
@@ -535,12 +628,10 @@ onBeforeUnmount(() => {
border: none !important;
}
/* 代码字体栈 */
:deep(.prose code) {
font-family: 'SF Mono', Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* 行内代码样式优化 */
:deep(.prose :not(pre) > code) {
color: #d4b383;
background-color: rgba(212, 179, 131, 0.1);
@@ -549,14 +640,13 @@ onBeforeUnmount(() => {
font-weight: 500;
}
/* 标题样式 - 增加 scroll-margin-top 以防止锚点跳转被顶部遮挡 */
:deep(.prose h1), :deep(.prose h2), :deep(.prose h3) {
color: #fff;
font-family: serif;
margin-top: 2em;
margin-bottom: 0.8em;
line-height: 1.3;
scroll-margin-top: 120px; /* 关键:确保点击目录跳转时有足够顶部留白 */
scroll-margin-top: 120px;
}
:deep(.prose h2) {
@@ -570,7 +660,6 @@ onBeforeUnmount(() => {
color: rgba(255, 255, 255, 0.9);
}
/* 链接样式 */
:deep(.prose a) {
color: #d4b383;
text-decoration: none;
@@ -583,7 +672,6 @@ onBeforeUnmount(() => {
border-bottom-style: solid;
}
/* 引用块样式 */
:deep(.prose blockquote) {
border-left: 3px solid #d4b383;
background: rgba(255, 255, 255, 0.02);
@@ -597,7 +685,6 @@ onBeforeUnmount(() => {
color: #d4b383;
}
/* 自定义滚动条 */
:deep(.code-block-wrapper::-webkit-scrollbar) {
height: 8px;
background-color: transparent;
@@ -619,4 +706,19 @@ onBeforeUnmount(() => {
:deep(.hljs) {
background: transparent !important;
}
/* 移动端动画 */
@keyframes slide-up {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-slide-up {
animation: slide-up 0.3s ease-out forwards;
}
</style>

View File

@@ -15,7 +15,7 @@
<div class="pt-8 flex items-center gap-6">
<button @click="$router.push('/works')" class="group flex items-center gap-2 text-white border-b border-white pb-1 hover:text-art-accent hover:border-art-accent transition-all">
<span>浏览作品集</span>
<i data-lucide="arrow-right" class="w-4 h-4 transform group-hover:translate-x-1 transition-transform"></i>
<Icon name="arrow-right" :size="16" class="transform group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
@@ -30,7 +30,7 @@
>
<div class="text-white/50 text-xs">最新作品</div>
<div class="font-serif text-3xl italic text-white">{{ latestWork.title }}</div>
<div class="flex justify-end"><i data-lucide="arrow-up-right" class="text-white"></i></div>
<div class="flex justify-end"><Icon name="arrow-up-right" :size="24" class="text-white" /></div>
</div>
</div>
</div>
@@ -63,8 +63,8 @@
class="art-card rounded-2xl p-8 flex flex-col justify-between group"
>
<div class="flex justify-between items-start">
<i data-lucide="code-2" class="w-8 h-8 text-white/40 group-hover:text-art-accent transition-colors"></i>
<i data-lucide="arrow-up-right" class="w-5 h-5 text-white/20"></i>
<Icon name="code-2" :size="32" class="text-white/40 group-hover:text-art-accent transition-colors" />
<Icon name="arrow-up-right" :size="20" class="text-white/20" />
</div>
<div>
<div class="text-4xl font-mono font-bold text-white mb-2">120+</div>
@@ -84,7 +84,7 @@
</div>
<div class="mt-4 flex items-center gap-2 text-sm font-medium text-white">
<span>访问代码实验室</span>
<i data-lucide="chevron-right" class="w-4 h-4 text-art-accent"></i>
<Icon name="chevron-right" :size="16" class="text-art-accent" />
</div>
</div>
<div
@@ -96,7 +96,7 @@
<p class="text-art-muted max-w-sm">不论是 WebGL 3D 交互网站还是高性能的 SaaS 管理系统我都能提供专业的解决方案</p>
</div>
<div class="h-16 w-16 rounded-full border border-white/20 flex items-center justify-center group-hover:bg-white group-hover:text-black transition-all duration-300">
<i data-lucide="arrow-right" class="w-6 h-6"></i>
<Icon name="arrow-right" :size="24" />
</div>
</div>
</div>
@@ -108,6 +108,7 @@
import { ref, onMounted } from 'vue'
import { fetchWorks, fetchPosts, Work, Post } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import Icon from '../components/Icon.vue'
const { initObserver } = useScrollAnimation()

View File

@@ -1,7 +1,7 @@
<template>
<section id="work-detail" class="work-detail-page page-section block">
<button @click="$router.push('/works')" class="fixed top-8 right-8 z-[60] mix-blend-difference text-white hover:scale-110 transition-transform">
<div class="rounded-full border border-white/20 p-4 backdrop-blur-md bg-white/5"><i data-lucide="x" class="w-6 h-6"></i></div>
<div class="rounded-full border border-white/20 p-4 backdrop-blur-md bg-white/5"><Icon name="x" :size="24" /></div>
</button>
<div class="fixed top-0 left-0 h-1 bg-art-accent z-[60] w-0 transition-all duration-100" id="scroll-progress"></div>
@@ -34,7 +34,7 @@
<!-- Play Button Overlay -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-24 h-24 rounded-full border border-white/20 bg-white/5 backdrop-blur-md flex items-center justify-center cursor-pointer hover:scale-110 hover:bg-white/10 transition-all duration-500 group-hover:border-art-accent/50 z-20">
<i data-lucide="play" class="w-8 h-8 text-white fill-white ml-1"></i>
<Icon name="play" :size="32" class="text-white fill-white ml-1" />
<div class="absolute inset-0 rounded-full border border-white/20 animate-pulse-slow"></div>
</div>
@@ -77,10 +77,10 @@
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<i data-lucide="globe" class="w-5 h-5"></i>
<Icon name="globe" :size="20" />
访问线上项目
</span>
<i data-lucide="arrow-up-right" class="w-5 h-5 text-white group-hover:rotate-45 transition-transform"></i>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
</a>
<!-- Demo Link -->
<a
@@ -91,10 +91,10 @@
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<i data-lucide="play-circle" class="w-5 h-5"></i>
<Icon name="play-circle" :size="20" />
查看演示
</span>
<i data-lucide="arrow-up-right" class="w-5 h-5 text-white group-hover:rotate-45 transition-transform"></i>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
</a>
<!-- GitHub Link -->
<a
@@ -105,10 +105,10 @@
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<i data-lucide="github" class="w-5 h-5"></i>
<Icon name="github" :size="20" />
查看源码
</span>
<i data-lucide="arrow-up-right" class="w-5 h-5 text-white group-hover:rotate-45 transition-transform"></i>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
</a>
</div>
</div>
@@ -152,6 +152,7 @@ import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchWork, Work } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import Icon from '../components/Icon.vue'
const route = useRoute()
const router = useRouter()
@@ -226,10 +227,7 @@ watch(() => route.params.id, (newId) => {
onMounted(() => {
fetchWorkDetails()
// 初始化Lucide图标
if (window.lucide) {
window.lucide.createIcons()
}
// Icons are now handled by Vue components
// 添加滚动事件监听
window.addEventListener('scroll', updateScrollProgress)
})

View File

@@ -77,7 +77,7 @@
@click.stop
class="inline-flex items-center gap-2 px-4 py-2 border border-white/10 rounded-full text-xs text-white/70 hover:border-art-accent hover:text-art-accent transition-all"
>
<i data-lucide="globe" class="w-3 h-3"></i>
<Icon name="globe" :size="12" />
线上项目
</a>
<a
@@ -87,7 +87,7 @@
@click.stop
class="inline-flex items-center gap-2 px-4 py-2 border border-white/10 rounded-full text-xs text-white/70 hover:border-art-accent hover:text-art-accent transition-all"
>
<i data-lucide="play-circle" class="w-3 h-3"></i>
<Icon name="play-circle" :size="12" />
演示
</a>
<a
@@ -97,14 +97,14 @@
@click.stop
class="inline-flex items-center gap-2 px-4 py-2 border border-white/10 rounded-full text-xs text-white/70 hover:border-art-accent hover:text-art-accent transition-all"
>
<i data-lucide="github" class="w-3 h-3"></i>
<Icon name="github" :size="12" />
源码
</a>
</div>
<!-- 查看详情按钮 -->
<div class="pt-4" :class="[index % 2 === 1 ? 'flex md:justify-end' : '']">
<span class="inline-flex items-center gap-2 text-sm font-medium text-white border-b border-transparent hover:border-art-accent hover:text-art-accent transition-all">
查看项目详情 <i data-lucide="arrow-right" class="w-4 h-4"></i>
查看项目详情 <Icon name="arrow-right" :size="16" />
</span>
</div>
</div>
@@ -118,6 +118,7 @@
import { ref, onMounted } from 'vue'
import { fetchWorks, Work } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import Icon from '../components/Icon.vue'
const works = ref<Work[]>([])
const loading = ref(false)
@@ -142,9 +143,6 @@ const fetchWorksData = async () => {
onMounted(() => {
fetchWorksData()
// 初始化Lucide图标
if (window.lucide) {
window.lucide.createIcons()
}
// Icons are now handled by Vue components
})
</script>