@jet-w/astro-blog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-FXPGR372.js +0 -0
- package/dist/chunk-GYLSY3OJ.js +173 -0
- package/dist/config/index.d.ts +166 -0
- package/dist/config/index.js +38 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +59 -0
- package/dist/types/index.d.ts +75 -0
- package/dist/types/index.js +1 -0
- package/package.json +84 -0
- package/src/components/EChartsCard.vue +118 -0
- package/src/components/Mermaid.vue +73 -0
- package/src/components/about/ContentCard.astro +27 -0
- package/src/components/about/IconCard.astro +77 -0
- package/src/components/about/SocialLinks.astro +54 -0
- package/src/components/about/TagCard.astro +65 -0
- package/src/components/about/TagGroup.astro +33 -0
- package/src/components/about/TimelineCard.astro +52 -0
- package/src/components/blog/FloatingToc.vue +198 -0
- package/src/components/blog/Hero.astro +147 -0
- package/src/components/blog/NavigationTabs.vue +245 -0
- package/src/components/blog/PostCard.astro +161 -0
- package/src/components/blog/PostNavigation.astro +106 -0
- package/src/components/blog/RelatedPosts.astro +175 -0
- package/src/components/blog/TableOfContents.astro +153 -0
- package/src/components/blog/TagCloud.astro +91 -0
- package/src/components/home/FeaturedPostsSection.astro +54 -0
- package/src/components/home/QuickNavSection.astro +81 -0
- package/src/components/home/RecentPostsSection.astro +52 -0
- package/src/components/home/StatsSection.astro +44 -0
- package/src/components/layout/Footer.astro +103 -0
- package/src/components/layout/Header.astro +68 -0
- package/src/components/layout/Sidebar.astro +594 -0
- package/src/components/media/Bilibili.astro +114 -0
- package/src/components/media/Slides.astro +313 -0
- package/src/components/media/Video.astro +111 -0
- package/src/components/media/VideoPlayer.astro +89 -0
- package/src/components/media/YouTube.astro +92 -0
- package/src/components/pte/StudyCalendar.vue +1348 -0
- package/src/components/ui/Icon.astro +187 -0
- package/src/components/ui/MobileMenu.vue +201 -0
- package/src/components/ui/Pagination.astro +143 -0
- package/src/components/ui/SearchBox.vue +179 -0
- package/src/components/ui/SearchInterface.vue +409 -0
- package/src/components/ui/SidebarToggle.vue +57 -0
- package/src/components/ui/ThemeToggle.vue +90 -0
- package/src/layouts/AboutLayout.astro +18 -0
- package/src/layouts/BaseLayout.astro +362 -0
- package/src/layouts/PageLayout.astro +217 -0
- package/src/layouts/SlidesLayout.astro +320 -0
- package/src/plugins/rehype-clean-containers.mjs +24 -0
- package/src/plugins/rehype-relative-links.mjs +43 -0
- package/src/plugins/rehype-tabs.mjs +116 -0
- package/src/plugins/remark-containers.mjs +407 -0
- package/src/plugins/remark-mermaid.mjs +46 -0
- package/src/styles/global.css +870 -0
- package/src/styles/slides.css +220 -0
- package/src/utils/sidebar.ts +492 -0
- package/templates/default/astro.config.mjs +51 -0
- package/templates/default/content/pages/about.mdx +93 -0
- package/templates/default/content/pages/index.mdx +20 -0
- package/templates/default/content/posts/blog_docs/01-quick-start.md +162 -0
- package/templates/default/content/posts/blog_docs/02-frontmatter.md +277 -0
- package/templates/default/content/posts/blog_docs/03-markdown-basic.md +350 -0
- package/templates/default/content/posts/blog_docs/04-containers.md +331 -0
- package/templates/default/content/posts/blog_docs/05-code-blocks.md +388 -0
- package/templates/default/content/posts/blog_docs/06-mermaid.md +431 -0
- package/templates/default/content/posts/blog_docs/07-video.md +243 -0
- package/templates/default/content/posts/blog_docs/08-latex.md +382 -0
- package/templates/default/content/posts/blog_docs/09-icons.md +326 -0
- package/templates/default/content/posts/blog_docs/10-sidebar.md +445 -0
- package/templates/default/content/posts/blog_docs/11-config.md +334 -0
- package/templates/default/content/posts/blog_docs/12-slides.mdx +552 -0
- package/templates/default/content/posts/blog_docs/README.md +151 -0
- package/templates/default/content/slides/demo.md +146 -0
- package/templates/default/content/slides/docs/basic-demo.md +35 -0
- package/templates/default/content/slides/docs/code-demo.md +62 -0
- package/templates/default/content/slides/docs/echarts-demo.md +139 -0
- package/templates/default/content/slides/docs/fragment-demo.md +35 -0
- package/templates/default/content/slides/docs/math-demo.md +48 -0
- package/templates/default/content/slides/docs/mermaid-demo.md +105 -0
- package/templates/default/content/slides/docs/theme-demo.md +38 -0
- package/templates/default/content/slides/docs/vertical-demo.md +50 -0
- package/templates/default/package.json +31 -0
- package/templates/default/public/favicon-bak.svg +4 -0
- package/templates/default/public/images/avatar.jpg +0 -0
- package/templates/default/public/images/avatar.svg +142 -0
- package/templates/default/public/js/mermaid-container.js +402 -0
- package/templates/default/public/js/mermaid-init.js +131 -0
- package/templates/default/public/js/mermaid-render.js +98 -0
- package/templates/default/public/js/mermaid-simple.js +95 -0
- package/templates/default/public/js/tabs-init.js +86 -0
- package/templates/default/public/media/individual_portfolio/INDIVIDUAL PORTFOLIO.png +0 -0
- package/templates/default/public/slides/plugin/highlight/highlight.js +5 -0
- package/templates/default/public/slides/plugin/highlight/monokai.css +71 -0
- package/templates/default/public/slides/plugin/markdown/markdown.js +7 -0
- package/templates/default/public/slides/plugin/math/math.js +1 -0
- package/templates/default/public/slides/plugin/notes/notes.js +1 -0
- package/templates/default/public/slides/reveal.css +9 -0
- package/templates/default/public/slides/reveal.js +9 -0
- package/templates/default/public/slides/theme/beige.css +366 -0
- package/templates/default/public/slides/theme/black-contrast.css +362 -0
- package/templates/default/public/slides/theme/black.css +359 -0
- package/templates/default/public/slides/theme/blood.css +392 -0
- package/templates/default/public/slides/theme/dracula.css +385 -0
- package/templates/default/public/slides/theme/league.css +368 -0
- package/templates/default/public/slides/theme/moon.css +362 -0
- package/templates/default/public/slides/theme/night.css +360 -0
- package/templates/default/public/slides/theme/serif.css +363 -0
- package/templates/default/public/slides/theme/simple.css +362 -0
- package/templates/default/public/slides/theme/sky.css +370 -0
- package/templates/default/public/slides/theme/solarized.css +363 -0
- package/templates/default/public/slides/theme/white-contrast.css +362 -0
- package/templates/default/public/slides/theme/white.css +359 -0
- package/templates/default/public/slides/theme/white_contrast_compact_verbatim_headers.css +360 -0
- package/templates/default/public/test-complete.html +43 -0
- package/templates/default/public/test-mermaid.html +124 -0
- package/templates/default/src/config/index.ts +114 -0
- package/templates/default/src/content.config.ts +96 -0
- package/templates/default/src/pages/[...slug].astro +27 -0
- package/templates/default/src/pages/archives/[year]/[month]/page/[page].astro +176 -0
- package/templates/default/src/pages/archives/[year]/[month].astro +158 -0
- package/templates/default/src/pages/archives/index.astro +210 -0
- package/templates/default/src/pages/categories/[category]/page/[page].astro +218 -0
- package/templates/default/src/pages/categories/[category].astro +198 -0
- package/templates/default/src/pages/categories/index.astro +190 -0
- package/templates/default/src/pages/container-test.astro +79 -0
- package/templates/default/src/pages/mermaid-direct.html +78 -0
- package/templates/default/src/pages/posts/[...slug].astro +335 -0
- package/templates/default/src/pages/posts/index.astro +541 -0
- package/templates/default/src/pages/posts/page/[page].astro +146 -0
- package/templates/default/src/pages/rss.xml.ts +28 -0
- package/templates/default/src/pages/search-index.json.ts +21 -0
- package/templates/default/src/pages/search.astro +50 -0
- package/templates/default/src/pages/slides/[...slug].astro +54 -0
- package/templates/default/src/pages/slides/index.astro +135 -0
- package/templates/default/src/pages/tags/[tag]/page/[page].astro +211 -0
- package/templates/default/src/pages/tags/[tag].astro +191 -0
- package/templates/default/src/pages/tags/index.astro +167 -0
- package/templates/default/tailwind.config.mjs +78 -0
- package/templates/default/tsconfig.json +9 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
currentPost: {
|
|
4
|
+
slug: string;
|
|
5
|
+
title: string;
|
|
6
|
+
tags?: string[];
|
|
7
|
+
categories?: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { currentPost } = Astro.props;
|
|
12
|
+
|
|
13
|
+
// 模拟获取相关文章
|
|
14
|
+
const allPosts = [
|
|
15
|
+
{
|
|
16
|
+
slug: 'astro-vs-nextjs',
|
|
17
|
+
title: 'Astro vs Next.js:静态站点生成器的对比',
|
|
18
|
+
description: '深入比较Astro和Next.js在性能、开发体验和生态系统方面的差异。',
|
|
19
|
+
pubDate: new Date('2025-01-07'),
|
|
20
|
+
tags: ['Astro', 'Next.js', 'SSG'],
|
|
21
|
+
categories: ['技术比较'],
|
|
22
|
+
readingTime: 8
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
slug: 'tailwind-best-practices',
|
|
26
|
+
title: 'Tailwind CSS最佳实践指南',
|
|
27
|
+
description: '总结使用Tailwind CSS开发时的最佳实践和常见问题解决方案。',
|
|
28
|
+
pubDate: new Date('2025-01-06'),
|
|
29
|
+
tags: ['Tailwind CSS', 'CSS', '最佳实践'],
|
|
30
|
+
categories: ['前端开发'],
|
|
31
|
+
readingTime: 6
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
slug: 'typescript-tips',
|
|
35
|
+
title: 'TypeScript高级技巧与实战',
|
|
36
|
+
description: '分享TypeScript在实际项目中的高级用法和技巧。',
|
|
37
|
+
pubDate: new Date('2025-01-05'),
|
|
38
|
+
tags: ['TypeScript', 'JavaScript'],
|
|
39
|
+
categories: ['编程语言'],
|
|
40
|
+
readingTime: 10
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// 计算相关度分数
|
|
45
|
+
function calculateRelevance(post: typeof allPosts[0]) {
|
|
46
|
+
let score = 0;
|
|
47
|
+
|
|
48
|
+
// 标签匹配
|
|
49
|
+
if (currentPost.tags && post.tags) {
|
|
50
|
+
const commonTags = currentPost.tags.filter(tag =>
|
|
51
|
+
post.tags!.includes(tag)
|
|
52
|
+
);
|
|
53
|
+
score += commonTags.length * 3;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 分类匹配
|
|
57
|
+
if (currentPost.categories && post.categories) {
|
|
58
|
+
const commonCategories = currentPost.categories.filter(category =>
|
|
59
|
+
post.categories!.includes(category)
|
|
60
|
+
);
|
|
61
|
+
score += commonCategories.length * 2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return score;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 获取相关文章(排除当前文章)
|
|
68
|
+
const relatedPosts = allPosts
|
|
69
|
+
.filter(post => post.slug !== currentPost.slug)
|
|
70
|
+
.map(post => ({
|
|
71
|
+
...post,
|
|
72
|
+
relevance: calculateRelevance(post)
|
|
73
|
+
}))
|
|
74
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
75
|
+
.slice(0, 3);
|
|
76
|
+
|
|
77
|
+
const formattedDate = (date: Date) => new Intl.DateTimeFormat('zh-CN', {
|
|
78
|
+
month: 'short',
|
|
79
|
+
day: 'numeric'
|
|
80
|
+
}).format(date);
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
{relatedPosts.length > 0 && (
|
|
84
|
+
<section class="mt-16">
|
|
85
|
+
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-8">
|
|
86
|
+
相关文章
|
|
87
|
+
</h2>
|
|
88
|
+
|
|
89
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
90
|
+
{relatedPosts.map((post) => (
|
|
91
|
+
<article class="group">
|
|
92
|
+
<a
|
|
93
|
+
href={`/posts/${post.slug}`}
|
|
94
|
+
class="card hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 block h-full"
|
|
95
|
+
>
|
|
96
|
+
<div class="flex flex-col h-full">
|
|
97
|
+
<!-- 元信息 -->
|
|
98
|
+
<div class="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 mb-3">
|
|
99
|
+
<time datetime={post.pubDate.toISOString()}>
|
|
100
|
+
{formattedDate(post.pubDate)}
|
|
101
|
+
</time>
|
|
102
|
+
{post.readingTime && (
|
|
103
|
+
<span>{post.readingTime}分钟</span>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- 标题 -->
|
|
108
|
+
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors mb-3 line-clamp-2">
|
|
109
|
+
{post.title}
|
|
110
|
+
</h3>
|
|
111
|
+
|
|
112
|
+
<!-- 描述 -->
|
|
113
|
+
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4 line-clamp-3 flex-1">
|
|
114
|
+
{post.description}
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<!-- 标签 -->
|
|
118
|
+
{post.tags && post.tags.length > 0 && (
|
|
119
|
+
<div class="flex flex-wrap gap-1 mb-3">
|
|
120
|
+
{post.tags.slice(0, 2).map((tag) => (
|
|
121
|
+
<span class="text-xs px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full">
|
|
122
|
+
{tag}
|
|
123
|
+
</span>
|
|
124
|
+
))}
|
|
125
|
+
{post.tags.length > 2 && (
|
|
126
|
+
<span class="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded-full">
|
|
127
|
+
+{post.tags.length - 2}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
<!-- 阅读链接 -->
|
|
134
|
+
<div class="flex items-center text-primary-500 group-hover:text-primary-600 transition-colors text-sm font-medium">
|
|
135
|
+
<span>阅读更多</span>
|
|
136
|
+
<svg class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
137
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
138
|
+
</svg>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</a>
|
|
142
|
+
</article>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- 查看更多按钮 -->
|
|
147
|
+
<div class="text-center mt-8">
|
|
148
|
+
<a
|
|
149
|
+
href="/posts"
|
|
150
|
+
class="btn-secondary inline-flex items-center space-x-2"
|
|
151
|
+
>
|
|
152
|
+
<span>查看更多文章</span>
|
|
153
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
154
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
155
|
+
</svg>
|
|
156
|
+
</a>
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
<style>
|
|
162
|
+
.line-clamp-2 {
|
|
163
|
+
display: -webkit-box;
|
|
164
|
+
-webkit-line-clamp: 2;
|
|
165
|
+
-webkit-box-orient: vertical;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.line-clamp-3 {
|
|
170
|
+
display: -webkit-box;
|
|
171
|
+
-webkit-line-clamp: 3;
|
|
172
|
+
-webkit-box-orient: vertical;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
}
|
|
175
|
+
</style>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
headings: Array<{
|
|
4
|
+
level: number;
|
|
5
|
+
text: string;
|
|
6
|
+
id: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { headings } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{headings.length > 0 && (
|
|
14
|
+
<nav class="sticky top-20 p-6" aria-label="目录">
|
|
15
|
+
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
|
|
16
|
+
目录
|
|
17
|
+
</h3>
|
|
18
|
+
|
|
19
|
+
<ol class="space-y-2 text-sm">
|
|
20
|
+
{headings.map((heading) => (
|
|
21
|
+
<li
|
|
22
|
+
class={`${
|
|
23
|
+
heading.level === 2 ? 'pl-0' :
|
|
24
|
+
heading.level === 3 ? 'pl-4' :
|
|
25
|
+
'pl-8'
|
|
26
|
+
}`}
|
|
27
|
+
>
|
|
28
|
+
<a
|
|
29
|
+
href={`#${heading.id}`}
|
|
30
|
+
class="block py-1 text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors toc-link"
|
|
31
|
+
data-heading-id={heading.id}
|
|
32
|
+
>
|
|
33
|
+
{heading.text}
|
|
34
|
+
</a>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ol>
|
|
38
|
+
|
|
39
|
+
<!-- 进度指示器 -->
|
|
40
|
+
<div class="mt-8 pt-4 border-t border-slate-200 dark:border-slate-700">
|
|
41
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 mb-2">阅读进度</div>
|
|
42
|
+
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2">
|
|
43
|
+
<div class="bg-primary-500 h-2 rounded-full transition-all duration-300" id="reading-progress" style="width: 0%"></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</nav>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
51
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
52
|
+
const headings = document.querySelectorAll('h2, h3, h4');
|
|
53
|
+
const progressBar = document.getElementById('reading-progress');
|
|
54
|
+
|
|
55
|
+
// 更新目录高亮
|
|
56
|
+
function updateTocHighlight() {
|
|
57
|
+
let current = '';
|
|
58
|
+
|
|
59
|
+
headings.forEach((heading) => {
|
|
60
|
+
const rect = heading.getBoundingClientRect();
|
|
61
|
+
if (rect.top <= 100) {
|
|
62
|
+
current = heading.id;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
tocLinks.forEach((link) => {
|
|
67
|
+
const linkId = link.getAttribute('data-heading-id');
|
|
68
|
+
if (linkId === current) {
|
|
69
|
+
link.classList.add('text-primary-500', 'font-medium');
|
|
70
|
+
link.classList.remove('text-slate-600', 'dark:text-slate-400');
|
|
71
|
+
} else {
|
|
72
|
+
link.classList.remove('text-primary-500', 'font-medium');
|
|
73
|
+
link.classList.add('text-slate-600', 'dark:text-slate-400');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 更新阅读进度
|
|
79
|
+
function updateReadingProgress() {
|
|
80
|
+
if (!progressBar) return;
|
|
81
|
+
|
|
82
|
+
const article = document.querySelector('main');
|
|
83
|
+
if (!article) return;
|
|
84
|
+
|
|
85
|
+
const articleTop = article.offsetTop;
|
|
86
|
+
const articleHeight = article.offsetHeight;
|
|
87
|
+
const windowTop = window.pageYOffset;
|
|
88
|
+
const windowHeight = window.innerHeight;
|
|
89
|
+
|
|
90
|
+
const progress = Math.min(
|
|
91
|
+
Math.max((windowTop - articleTop + windowHeight / 2) / articleHeight, 0),
|
|
92
|
+
1
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
progressBar.style.width = `${progress * 100}%`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 平滑滚动到标题
|
|
99
|
+
tocLinks.forEach((link) => {
|
|
100
|
+
link.addEventListener('click', (e) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
const targetId = link.getAttribute('data-heading-id');
|
|
103
|
+
const targetElement = targetId ? document.getElementById(targetId) : null;
|
|
104
|
+
|
|
105
|
+
if (targetElement) {
|
|
106
|
+
const offsetTop = targetElement.offsetTop - 80; // 考虑固定头部的高度
|
|
107
|
+
window.scrollTo({
|
|
108
|
+
top: offsetTop,
|
|
109
|
+
behavior: 'smooth'
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 监听滚动事件
|
|
116
|
+
let ticking = false;
|
|
117
|
+
|
|
118
|
+
function handleScroll() {
|
|
119
|
+
if (!ticking) {
|
|
120
|
+
requestAnimationFrame(() => {
|
|
121
|
+
updateTocHighlight();
|
|
122
|
+
updateReadingProgress();
|
|
123
|
+
ticking = false;
|
|
124
|
+
});
|
|
125
|
+
ticking = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
window.addEventListener('scroll', handleScroll);
|
|
130
|
+
|
|
131
|
+
// 初始化
|
|
132
|
+
updateTocHighlight();
|
|
133
|
+
updateReadingProgress();
|
|
134
|
+
});
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.toc-link {
|
|
139
|
+
position: relative;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.toc-link.text-primary-500::before {
|
|
143
|
+
content: '';
|
|
144
|
+
position: absolute;
|
|
145
|
+
left: -12px;
|
|
146
|
+
top: 50%;
|
|
147
|
+
transform: translateY(-50%);
|
|
148
|
+
width: 4px;
|
|
149
|
+
height: 16px;
|
|
150
|
+
background-color: theme('colors.primary.500');
|
|
151
|
+
border-radius: 2px;
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
tags: Array<{
|
|
4
|
+
name: string;
|
|
5
|
+
count: number;
|
|
6
|
+
slug: string;
|
|
7
|
+
}>;
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
minSize?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { tags, maxSize = 2.5, minSize = 0.8 } = Astro.props;
|
|
13
|
+
|
|
14
|
+
// 计算字体大小
|
|
15
|
+
const maxCount = Math.max(...tags.map(tag => tag.count));
|
|
16
|
+
const minCount = Math.min(...tags.map(tag => tag.count));
|
|
17
|
+
|
|
18
|
+
const getFontSize = (count: number) => {
|
|
19
|
+
if (maxCount === minCount) return (maxSize + minSize) / 2;
|
|
20
|
+
const ratio = (count - minCount) / (maxCount - minCount);
|
|
21
|
+
return minSize + ratio * (maxSize - minSize);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 生成随机颜色类
|
|
25
|
+
const getColorClass = (index: number) => {
|
|
26
|
+
const colors = [
|
|
27
|
+
'text-primary-500 hover:text-primary-600',
|
|
28
|
+
'text-secondary-500 hover:text-secondary-600',
|
|
29
|
+
'text-accent-500 hover:text-accent-600',
|
|
30
|
+
'text-blue-500 hover:text-blue-600',
|
|
31
|
+
'text-green-500 hover:text-green-600',
|
|
32
|
+
'text-purple-500 hover:text-purple-600',
|
|
33
|
+
'text-pink-500 hover:text-pink-600',
|
|
34
|
+
'text-indigo-500 hover:text-indigo-600',
|
|
35
|
+
'text-red-500 hover:text-red-600',
|
|
36
|
+
'text-yellow-500 hover:text-yellow-600',
|
|
37
|
+
'text-teal-500 hover:text-teal-600',
|
|
38
|
+
'text-orange-500 hover:text-orange-600'
|
|
39
|
+
];
|
|
40
|
+
return colors[index % colors.length];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 随机打乱标签顺序以增加视觉趣味性
|
|
44
|
+
const shuffledTags = [...tags].sort(() => Math.random() - 0.5);
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<div class="tag-cloud p-8 bg-slate-50 dark:bg-slate-800 rounded-xl">
|
|
48
|
+
<div class="flex flex-wrap items-center justify-center gap-3 leading-relaxed">
|
|
49
|
+
{shuffledTags.map((tag, index) => (
|
|
50
|
+
<a
|
|
51
|
+
href={`/tags/${tag.slug}`}
|
|
52
|
+
class={`inline-block font-medium transition-all duration-300 hover:scale-110 ${getColorClass(index)}`}
|
|
53
|
+
style={`font-size: ${getFontSize(tag.count)}rem`}
|
|
54
|
+
title={`${tag.name} (${tag.count} 篇文章)`}
|
|
55
|
+
>
|
|
56
|
+
{tag.name}
|
|
57
|
+
</a>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- 图例 -->
|
|
62
|
+
<div class="mt-6 pt-4 border-t border-slate-200 dark:border-slate-700">
|
|
63
|
+
<div class="flex items-center justify-center space-x-8 text-sm text-slate-600 dark:text-slate-400">
|
|
64
|
+
<div class="flex items-center space-x-2">
|
|
65
|
+
<span class="text-xs">文字大小表示文章数量</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="flex items-center space-x-2">
|
|
68
|
+
<span class="text-xs">共 {tags.length} 个标签</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="flex items-center space-x-2">
|
|
71
|
+
<span class="text-xs">最多 {maxCount} 篇</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.tag-cloud {
|
|
79
|
+
background:
|
|
80
|
+
radial-gradient(circle at 20% 80%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
|
|
81
|
+
radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.1) 0%, transparent 50%),
|
|
82
|
+
radial-gradient(circle at 40% 40%, rgba(99, 102, 241, 0.1) 0%, transparent 50%);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dark .tag-cloud {
|
|
86
|
+
background:
|
|
87
|
+
radial-gradient(circle at 20% 80%, rgba(59, 130, 246, 0.05) 0%, transparent 50%),
|
|
88
|
+
radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.05) 0%, transparent 50%),
|
|
89
|
+
radial-gradient(circle at 40% 40%, rgba(99, 102, 241, 0.05) 0%, transparent 50%);
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 首页特色文章组件
|
|
4
|
+
*/
|
|
5
|
+
import PostCard from '../../components/blog/PostCard.astro';
|
|
6
|
+
import { getCollection } from 'astro:content';
|
|
7
|
+
|
|
8
|
+
export interface Props {
|
|
9
|
+
count?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { count = 3 } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const allPosts = await getCollection('posts');
|
|
15
|
+
const publishedPosts = allPosts
|
|
16
|
+
.filter(post => !post.data.draft)
|
|
17
|
+
.sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
|
|
18
|
+
|
|
19
|
+
const featuredPosts = publishedPosts.slice(0, count).map(post => ({
|
|
20
|
+
slug: post.id.toLowerCase(),
|
|
21
|
+
title: post.data.title,
|
|
22
|
+
description: post.data.description,
|
|
23
|
+
pubDate: post.data.pubDate,
|
|
24
|
+
tags: post.data.tags,
|
|
25
|
+
categories: post.data.categories,
|
|
26
|
+
author: post.data.author,
|
|
27
|
+
readingTime: 5
|
|
28
|
+
}));
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
{featuredPosts.length > 0 && (
|
|
32
|
+
<section class="mb-16">
|
|
33
|
+
<div class="flex items-center justify-between mb-8">
|
|
34
|
+
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
35
|
+
特色文章
|
|
36
|
+
</h2>
|
|
37
|
+
<a
|
|
38
|
+
href="/posts"
|
|
39
|
+
class="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 font-medium text-sm flex items-center space-x-1 transition-colors"
|
|
40
|
+
>
|
|
41
|
+
<span>查看全部</span>
|
|
42
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
43
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
44
|
+
</svg>
|
|
45
|
+
</a>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
49
|
+
{featuredPosts.map((post) => (
|
|
50
|
+
<PostCard post={post} featured={true} />
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
)}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 首页快速导航组件
|
|
4
|
+
*/
|
|
5
|
+
import NavigationTabs from '../../components/blog/NavigationTabs.vue';
|
|
6
|
+
import { getCollection } from 'astro:content';
|
|
7
|
+
|
|
8
|
+
const allPosts = await getCollection('posts');
|
|
9
|
+
const publishedPosts = allPosts
|
|
10
|
+
.filter(post => !post.data.draft)
|
|
11
|
+
.sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
|
|
12
|
+
|
|
13
|
+
const posts = publishedPosts.map(post => ({
|
|
14
|
+
slug: post.id.toLowerCase(),
|
|
15
|
+
title: post.data.title,
|
|
16
|
+
pubDate: post.data.pubDate,
|
|
17
|
+
tags: post.data.tags,
|
|
18
|
+
categories: post.data.categories,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// 构建标签数据
|
|
22
|
+
const tagCounts: Record<string, number> = {};
|
|
23
|
+
posts.forEach(post => {
|
|
24
|
+
(post.tags || []).forEach(tag => {
|
|
25
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
const tagsData = Object.entries(tagCounts)
|
|
29
|
+
.map(([name, count]) => ({ name, count }))
|
|
30
|
+
.sort((a, b) => b.count - a.count);
|
|
31
|
+
|
|
32
|
+
// 构建分类数据
|
|
33
|
+
const categoryCounts: Record<string, number> = {};
|
|
34
|
+
posts.forEach(post => {
|
|
35
|
+
(post.categories || []).forEach(category => {
|
|
36
|
+
categoryCounts[category] = (categoryCounts[category] || 0) + 1;
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
const categoriesData = Object.entries(categoryCounts)
|
|
40
|
+
.map(([name, count]) => ({ name, count }))
|
|
41
|
+
.sort((a, b) => b.count - a.count);
|
|
42
|
+
|
|
43
|
+
// 构建归档数据
|
|
44
|
+
const archiveCounts: Record<string, number> = {};
|
|
45
|
+
posts.forEach(post => {
|
|
46
|
+
if (post.pubDate) {
|
|
47
|
+
const year = post.pubDate.getFullYear().toString();
|
|
48
|
+
const month = (post.pubDate.getMonth() + 1).toString().padStart(2, '0');
|
|
49
|
+
const key = `${year}-${month}`;
|
|
50
|
+
archiveCounts[key] = (archiveCounts[key] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const archivesData = Object.entries(archiveCounts)
|
|
54
|
+
.map(([key, count]) => {
|
|
55
|
+
const [year, month] = key.split('-');
|
|
56
|
+
return { year, month, key, count };
|
|
57
|
+
})
|
|
58
|
+
.sort((a, b) => b.key.localeCompare(a.key));
|
|
59
|
+
|
|
60
|
+
// 构建时间轴数据(最近10篇)
|
|
61
|
+
const timelineData = posts.slice(0, 10).map(post => ({
|
|
62
|
+
slug: post.slug,
|
|
63
|
+
title: post.title,
|
|
64
|
+
pubDate: post.pubDate?.toISOString() || ''
|
|
65
|
+
}));
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
<section class="mb-16">
|
|
69
|
+
<div class="flex items-center justify-between mb-8">
|
|
70
|
+
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
71
|
+
快速导航
|
|
72
|
+
</h2>
|
|
73
|
+
</div>
|
|
74
|
+
<NavigationTabs
|
|
75
|
+
client:load
|
|
76
|
+
tags={tagsData}
|
|
77
|
+
archives={archivesData}
|
|
78
|
+
categories={categoriesData}
|
|
79
|
+
timeline={timelineData}
|
|
80
|
+
/>
|
|
81
|
+
</section>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 首页最新文章组件
|
|
4
|
+
*/
|
|
5
|
+
import PostCard from '../../components/blog/PostCard.astro';
|
|
6
|
+
import { getCollection } from 'astro:content';
|
|
7
|
+
|
|
8
|
+
export interface Props {
|
|
9
|
+
count?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { count = 6 } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const allPosts = await getCollection('posts');
|
|
15
|
+
const publishedPosts = allPosts
|
|
16
|
+
.filter(post => !post.data.draft)
|
|
17
|
+
.sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
|
|
18
|
+
|
|
19
|
+
const recentPosts = publishedPosts.slice(0, count).map(post => ({
|
|
20
|
+
slug: post.id.toLowerCase(),
|
|
21
|
+
title: post.data.title,
|
|
22
|
+
description: post.data.description,
|
|
23
|
+
pubDate: post.data.pubDate,
|
|
24
|
+
tags: post.data.tags,
|
|
25
|
+
categories: post.data.categories,
|
|
26
|
+
author: post.data.author,
|
|
27
|
+
readingTime: 5
|
|
28
|
+
}));
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<section class="mb-16">
|
|
32
|
+
<div class="flex items-center justify-between mb-8">
|
|
33
|
+
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
34
|
+
最新文章
|
|
35
|
+
</h2>
|
|
36
|
+
<a
|
|
37
|
+
href="/posts"
|
|
38
|
+
class="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 font-medium text-sm flex items-center space-x-1 transition-colors"
|
|
39
|
+
>
|
|
40
|
+
<span>查看全部</span>
|
|
41
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
42
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
43
|
+
</svg>
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="space-y-6">
|
|
48
|
+
{recentPosts.map((post) => (
|
|
49
|
+
<PostCard post={post} layout="horizontal" />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 首页统计信息组件
|
|
4
|
+
*/
|
|
5
|
+
import { getCollection } from 'astro:content';
|
|
6
|
+
|
|
7
|
+
const allPosts = await getCollection('posts');
|
|
8
|
+
const publishedPosts = allPosts.filter(post => !post.data.draft);
|
|
9
|
+
|
|
10
|
+
const totalPosts = publishedPosts.length;
|
|
11
|
+
const allTags = new Set(publishedPosts.flatMap(post => post.data.tags || []));
|
|
12
|
+
const allCategories = new Set(publishedPosts.flatMap(post => post.data.categories || []));
|
|
13
|
+
const totalTags = allTags.size;
|
|
14
|
+
const totalCategories = allCategories.size;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<section class="mb-16">
|
|
18
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
|
19
|
+
<div class="card text-center">
|
|
20
|
+
<div class="text-3xl font-bold text-primary-500 mb-2">
|
|
21
|
+
{totalPosts}
|
|
22
|
+
</div>
|
|
23
|
+
<div class="text-sm text-slate-600 dark:text-slate-400">
|
|
24
|
+
文章总数
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="card text-center">
|
|
28
|
+
<div class="text-3xl font-bold text-primary-500 mb-2">
|
|
29
|
+
{totalTags}
|
|
30
|
+
</div>
|
|
31
|
+
<div class="text-sm text-slate-600 dark:text-slate-400">
|
|
32
|
+
标签数量
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="card text-center">
|
|
36
|
+
<div class="text-3xl font-bold text-primary-500 mb-2">
|
|
37
|
+
{totalCategories}
|
|
38
|
+
</div>
|
|
39
|
+
<div class="text-sm text-slate-600 dark:text-slate-400">
|
|
40
|
+
分类数量
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|