@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.
Files changed (140) hide show
  1. package/dist/chunk-FXPGR372.js +0 -0
  2. package/dist/chunk-GYLSY3OJ.js +173 -0
  3. package/dist/config/index.d.ts +166 -0
  4. package/dist/config/index.js +38 -0
  5. package/dist/index.d.ts +34 -0
  6. package/dist/index.js +59 -0
  7. package/dist/types/index.d.ts +75 -0
  8. package/dist/types/index.js +1 -0
  9. package/package.json +84 -0
  10. package/src/components/EChartsCard.vue +118 -0
  11. package/src/components/Mermaid.vue +73 -0
  12. package/src/components/about/ContentCard.astro +27 -0
  13. package/src/components/about/IconCard.astro +77 -0
  14. package/src/components/about/SocialLinks.astro +54 -0
  15. package/src/components/about/TagCard.astro +65 -0
  16. package/src/components/about/TagGroup.astro +33 -0
  17. package/src/components/about/TimelineCard.astro +52 -0
  18. package/src/components/blog/FloatingToc.vue +198 -0
  19. package/src/components/blog/Hero.astro +147 -0
  20. package/src/components/blog/NavigationTabs.vue +245 -0
  21. package/src/components/blog/PostCard.astro +161 -0
  22. package/src/components/blog/PostNavigation.astro +106 -0
  23. package/src/components/blog/RelatedPosts.astro +175 -0
  24. package/src/components/blog/TableOfContents.astro +153 -0
  25. package/src/components/blog/TagCloud.astro +91 -0
  26. package/src/components/home/FeaturedPostsSection.astro +54 -0
  27. package/src/components/home/QuickNavSection.astro +81 -0
  28. package/src/components/home/RecentPostsSection.astro +52 -0
  29. package/src/components/home/StatsSection.astro +44 -0
  30. package/src/components/layout/Footer.astro +103 -0
  31. package/src/components/layout/Header.astro +68 -0
  32. package/src/components/layout/Sidebar.astro +594 -0
  33. package/src/components/media/Bilibili.astro +114 -0
  34. package/src/components/media/Slides.astro +313 -0
  35. package/src/components/media/Video.astro +111 -0
  36. package/src/components/media/VideoPlayer.astro +89 -0
  37. package/src/components/media/YouTube.astro +92 -0
  38. package/src/components/pte/StudyCalendar.vue +1348 -0
  39. package/src/components/ui/Icon.astro +187 -0
  40. package/src/components/ui/MobileMenu.vue +201 -0
  41. package/src/components/ui/Pagination.astro +143 -0
  42. package/src/components/ui/SearchBox.vue +179 -0
  43. package/src/components/ui/SearchInterface.vue +409 -0
  44. package/src/components/ui/SidebarToggle.vue +57 -0
  45. package/src/components/ui/ThemeToggle.vue +90 -0
  46. package/src/layouts/AboutLayout.astro +18 -0
  47. package/src/layouts/BaseLayout.astro +362 -0
  48. package/src/layouts/PageLayout.astro +217 -0
  49. package/src/layouts/SlidesLayout.astro +320 -0
  50. package/src/plugins/rehype-clean-containers.mjs +24 -0
  51. package/src/plugins/rehype-relative-links.mjs +43 -0
  52. package/src/plugins/rehype-tabs.mjs +116 -0
  53. package/src/plugins/remark-containers.mjs +407 -0
  54. package/src/plugins/remark-mermaid.mjs +46 -0
  55. package/src/styles/global.css +870 -0
  56. package/src/styles/slides.css +220 -0
  57. package/src/utils/sidebar.ts +492 -0
  58. package/templates/default/astro.config.mjs +51 -0
  59. package/templates/default/content/pages/about.mdx +93 -0
  60. package/templates/default/content/pages/index.mdx +20 -0
  61. package/templates/default/content/posts/blog_docs/01-quick-start.md +162 -0
  62. package/templates/default/content/posts/blog_docs/02-frontmatter.md +277 -0
  63. package/templates/default/content/posts/blog_docs/03-markdown-basic.md +350 -0
  64. package/templates/default/content/posts/blog_docs/04-containers.md +331 -0
  65. package/templates/default/content/posts/blog_docs/05-code-blocks.md +388 -0
  66. package/templates/default/content/posts/blog_docs/06-mermaid.md +431 -0
  67. package/templates/default/content/posts/blog_docs/07-video.md +243 -0
  68. package/templates/default/content/posts/blog_docs/08-latex.md +382 -0
  69. package/templates/default/content/posts/blog_docs/09-icons.md +326 -0
  70. package/templates/default/content/posts/blog_docs/10-sidebar.md +445 -0
  71. package/templates/default/content/posts/blog_docs/11-config.md +334 -0
  72. package/templates/default/content/posts/blog_docs/12-slides.mdx +552 -0
  73. package/templates/default/content/posts/blog_docs/README.md +151 -0
  74. package/templates/default/content/slides/demo.md +146 -0
  75. package/templates/default/content/slides/docs/basic-demo.md +35 -0
  76. package/templates/default/content/slides/docs/code-demo.md +62 -0
  77. package/templates/default/content/slides/docs/echarts-demo.md +139 -0
  78. package/templates/default/content/slides/docs/fragment-demo.md +35 -0
  79. package/templates/default/content/slides/docs/math-demo.md +48 -0
  80. package/templates/default/content/slides/docs/mermaid-demo.md +105 -0
  81. package/templates/default/content/slides/docs/theme-demo.md +38 -0
  82. package/templates/default/content/slides/docs/vertical-demo.md +50 -0
  83. package/templates/default/package.json +31 -0
  84. package/templates/default/public/favicon-bak.svg +4 -0
  85. package/templates/default/public/images/avatar.jpg +0 -0
  86. package/templates/default/public/images/avatar.svg +142 -0
  87. package/templates/default/public/js/mermaid-container.js +402 -0
  88. package/templates/default/public/js/mermaid-init.js +131 -0
  89. package/templates/default/public/js/mermaid-render.js +98 -0
  90. package/templates/default/public/js/mermaid-simple.js +95 -0
  91. package/templates/default/public/js/tabs-init.js +86 -0
  92. package/templates/default/public/media/individual_portfolio/INDIVIDUAL PORTFOLIO.png +0 -0
  93. package/templates/default/public/slides/plugin/highlight/highlight.js +5 -0
  94. package/templates/default/public/slides/plugin/highlight/monokai.css +71 -0
  95. package/templates/default/public/slides/plugin/markdown/markdown.js +7 -0
  96. package/templates/default/public/slides/plugin/math/math.js +1 -0
  97. package/templates/default/public/slides/plugin/notes/notes.js +1 -0
  98. package/templates/default/public/slides/reveal.css +9 -0
  99. package/templates/default/public/slides/reveal.js +9 -0
  100. package/templates/default/public/slides/theme/beige.css +366 -0
  101. package/templates/default/public/slides/theme/black-contrast.css +362 -0
  102. package/templates/default/public/slides/theme/black.css +359 -0
  103. package/templates/default/public/slides/theme/blood.css +392 -0
  104. package/templates/default/public/slides/theme/dracula.css +385 -0
  105. package/templates/default/public/slides/theme/league.css +368 -0
  106. package/templates/default/public/slides/theme/moon.css +362 -0
  107. package/templates/default/public/slides/theme/night.css +360 -0
  108. package/templates/default/public/slides/theme/serif.css +363 -0
  109. package/templates/default/public/slides/theme/simple.css +362 -0
  110. package/templates/default/public/slides/theme/sky.css +370 -0
  111. package/templates/default/public/slides/theme/solarized.css +363 -0
  112. package/templates/default/public/slides/theme/white-contrast.css +362 -0
  113. package/templates/default/public/slides/theme/white.css +359 -0
  114. package/templates/default/public/slides/theme/white_contrast_compact_verbatim_headers.css +360 -0
  115. package/templates/default/public/test-complete.html +43 -0
  116. package/templates/default/public/test-mermaid.html +124 -0
  117. package/templates/default/src/config/index.ts +114 -0
  118. package/templates/default/src/content.config.ts +96 -0
  119. package/templates/default/src/pages/[...slug].astro +27 -0
  120. package/templates/default/src/pages/archives/[year]/[month]/page/[page].astro +176 -0
  121. package/templates/default/src/pages/archives/[year]/[month].astro +158 -0
  122. package/templates/default/src/pages/archives/index.astro +210 -0
  123. package/templates/default/src/pages/categories/[category]/page/[page].astro +218 -0
  124. package/templates/default/src/pages/categories/[category].astro +198 -0
  125. package/templates/default/src/pages/categories/index.astro +190 -0
  126. package/templates/default/src/pages/container-test.astro +79 -0
  127. package/templates/default/src/pages/mermaid-direct.html +78 -0
  128. package/templates/default/src/pages/posts/[...slug].astro +335 -0
  129. package/templates/default/src/pages/posts/index.astro +541 -0
  130. package/templates/default/src/pages/posts/page/[page].astro +146 -0
  131. package/templates/default/src/pages/rss.xml.ts +28 -0
  132. package/templates/default/src/pages/search-index.json.ts +21 -0
  133. package/templates/default/src/pages/search.astro +50 -0
  134. package/templates/default/src/pages/slides/[...slug].astro +54 -0
  135. package/templates/default/src/pages/slides/index.astro +135 -0
  136. package/templates/default/src/pages/tags/[tag]/page/[page].astro +211 -0
  137. package/templates/default/src/pages/tags/[tag].astro +191 -0
  138. package/templates/default/src/pages/tags/index.astro +167 -0
  139. package/templates/default/tailwind.config.mjs +78 -0
  140. package/templates/default/tsconfig.json +9 -0
@@ -0,0 +1,541 @@
1
+ ---
2
+ import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
3
+ import PostCard from '@jet-w/astro-blog/components/blog/PostCard.astro';
4
+ import Pagination from '@jet-w/astro-blog/components/ui/Pagination.astro';
5
+ import NavigationTabs from '@jet-w/astro-blog/components/blog/NavigationTabs.vue';
6
+ import { getCollection } from 'astro:content';
7
+
8
+ // 从新的内容目录获取文章数据
9
+ const allPostsCollection = await getCollection('posts');
10
+ const publishedPosts = allPostsCollection
11
+ .filter(post => !post.data.draft)
12
+ .sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
13
+
14
+ const allPosts = publishedPosts.map(post => ({
15
+ slug: post.id.toLowerCase(),
16
+ title: post.data.title,
17
+ description: post.data.description,
18
+ pubDate: post.data.pubDate,
19
+ tags: post.data.tags,
20
+ categories: post.data.categories,
21
+ author: post.data.author,
22
+ readingTime: 5,
23
+ image: post.data.image
24
+ }));
25
+
26
+ // 构建标签数据
27
+ const tagCounts: Record<string, number> = {};
28
+ allPosts.forEach(post => {
29
+ (post.tags || []).forEach(tag => {
30
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
31
+ });
32
+ });
33
+ const tagsData = Object.entries(tagCounts)
34
+ .map(([name, count]) => ({ name, count }))
35
+ .sort((a, b) => b.count - a.count);
36
+
37
+ // 构建分类数据
38
+ const categoryCounts: Record<string, number> = {};
39
+ allPosts.forEach(post => {
40
+ (post.categories || []).forEach(category => {
41
+ categoryCounts[category] = (categoryCounts[category] || 0) + 1;
42
+ });
43
+ });
44
+ const categoriesData = Object.entries(categoryCounts)
45
+ .map(([name, count]) => ({ name, count }))
46
+ .sort((a, b) => b.count - a.count);
47
+
48
+ // 构建归档数据
49
+ const archiveCounts: Record<string, number> = {};
50
+ allPosts.forEach(post => {
51
+ if (post.pubDate) {
52
+ const year = post.pubDate.getFullYear().toString();
53
+ const month = (post.pubDate.getMonth() + 1).toString().padStart(2, '0');
54
+ const key = `${year}-${month}`;
55
+ archiveCounts[key] = (archiveCounts[key] || 0) + 1;
56
+ }
57
+ });
58
+ const archivesData = Object.entries(archiveCounts)
59
+ .map(([key, count]) => {
60
+ const [year, month] = key.split('-');
61
+ return { year, month, key, count };
62
+ })
63
+ .sort((a, b) => b.key.localeCompare(a.key));
64
+
65
+ // 构建时间轴数据(最近10篇)
66
+ const timelineData = allPosts.slice(0, 10).map(post => ({
67
+ slug: post.slug,
68
+ title: post.title,
69
+ pubDate: post.pubDate?.toISOString() || ''
70
+ }));
71
+
72
+ // 构建层级结构(用于列表模式)
73
+ interface TreeNode {
74
+ name: string;
75
+ slug?: string;
76
+ title?: string;
77
+ description?: string;
78
+ pubDate?: Date;
79
+ children: TreeNode[];
80
+ isFolder: boolean;
81
+ isReadme?: boolean;
82
+ }
83
+
84
+ const buildTree = () => {
85
+ const tree: TreeNode[] = [];
86
+ const folderTitles: Record<string, string> = {};
87
+
88
+ // 收集文件夹标题(从 README)
89
+ publishedPosts.forEach(post => {
90
+ const pathParts = post.id.split('/');
91
+ const fileName = pathParts[pathParts.length - 1].toLowerCase();
92
+ if (fileName === 'readme' || fileName === 'readme.md') {
93
+ const folderPath = pathParts.slice(0, -1).join('/');
94
+ if (folderPath && post.data.title) {
95
+ folderTitles[folderPath] = post.data.title;
96
+ }
97
+ }
98
+ });
99
+
100
+ // 构建树
101
+ publishedPosts.forEach(post => {
102
+ const pathParts = post.id.split('/');
103
+ let currentLevel = tree;
104
+ let currentPath = '';
105
+
106
+ pathParts.forEach((part, index) => {
107
+ const isLast = index === pathParts.length - 1;
108
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
109
+ const existing = currentLevel.find(n => n.name === part);
110
+ const isReadme = isLast && (part.toLowerCase() === 'readme' || part.toLowerCase() === 'readme.md');
111
+
112
+ if (existing) {
113
+ if (isLast) {
114
+ existing.slug = post.id;
115
+ existing.title = post.data.title;
116
+ existing.description = post.data.description;
117
+ existing.pubDate = post.data.pubDate;
118
+ existing.isReadme = isReadme;
119
+ }
120
+ currentLevel = existing.children;
121
+ } else {
122
+ const folderPath = pathParts.slice(0, index + 1).join('/');
123
+ const newNode: TreeNode = {
124
+ name: part,
125
+ slug: isLast ? post.id : undefined,
126
+ title: isLast ? post.data.title : folderTitles[folderPath],
127
+ description: isLast ? post.data.description : undefined,
128
+ pubDate: isLast ? post.data.pubDate : undefined,
129
+ children: [],
130
+ isFolder: !isLast,
131
+ isReadme: isReadme
132
+ };
133
+ currentLevel.push(newNode);
134
+ currentLevel = newNode.children;
135
+ }
136
+ });
137
+ });
138
+
139
+ // 排序并过滤 README
140
+ const sortTree = (nodes: TreeNode[]): TreeNode[] => {
141
+ return nodes
142
+ .filter(node => !node.isReadme)
143
+ .sort((a, b) => {
144
+ if (a.isFolder && !b.isFolder) return -1;
145
+ if (!a.isFolder && b.isFolder) return 1;
146
+ return a.name.localeCompare(b.name, 'zh-CN', { numeric: true });
147
+ })
148
+ .map(node => ({
149
+ ...node,
150
+ children: sortTree(node.children)
151
+ }));
152
+ };
153
+
154
+ return sortTree(tree);
155
+ };
156
+
157
+ const postTree = buildTree();
158
+
159
+ // 分页逻辑
160
+ const currentPage = 1;
161
+ const postsPerPage = 10;
162
+ const totalPosts = allPosts.length;
163
+ const totalPages = Math.ceil(totalPosts / postsPerPage);
164
+ const startIndex = (currentPage - 1) * postsPerPage;
165
+ const endIndex = startIndex + postsPerPage;
166
+ const posts = allPosts.slice(startIndex, endIndex);
167
+
168
+ // 获取所有标签和分类用于筛选
169
+ const allTags = [...new Set(allPosts.flatMap(post => post.tags || []))];
170
+ const allCategories = [...new Set(allPosts.flatMap(post => post.categories || []))];
171
+
172
+ // 格式化日期
173
+ const formatDate = (date?: Date) => {
174
+ if (!date) return '';
175
+ return new Intl.DateTimeFormat('zh-CN', {
176
+ year: 'numeric',
177
+ month: '2-digit',
178
+ day: '2-digit'
179
+ }).format(date);
180
+ };
181
+ ---
182
+
183
+ <PageLayout
184
+ title="文章列表"
185
+ description="浏览所有技术文章和分享内容"
186
+ showSidebar={true}
187
+ >
188
+ <!-- 页面头部 -->
189
+ <div class="mb-12">
190
+ <div class="text-center">
191
+ <h1 class="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
192
+ 文章列表
193
+ </h1>
194
+ <p class="text-xl text-slate-600 dark:text-slate-400 mb-8">
195
+ 分享技术思考和学习心得
196
+ </p>
197
+
198
+ <!-- 统计信息 -->
199
+ <div class="inline-flex items-center space-x-6 text-sm text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-800 px-6 py-3 rounded-lg">
200
+ <span>共 {totalPosts} 篇文章</span>
201
+ <span>•</span>
202
+ <span>{allTags.length} 个标签</span>
203
+ <span>•</span>
204
+ <span>{allCategories.length} 个分类</span>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- 导航标签卡片
210
+ <div class="mb-8">
211
+ <NavigationTabs
212
+ client:load
213
+ tags={tagsData}
214
+ archives={archivesData}
215
+ categories={categoriesData}
216
+ timeline={timelineData}
217
+ />
218
+ </div> -->
219
+
220
+ <!-- 筛选器 -->
221
+ <div class="mb-8 p-6 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
222
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
223
+ <!-- 按标签筛选 -->
224
+ <div>
225
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">
226
+ 按标签筛选
227
+ </h3>
228
+ <div class="flex flex-wrap gap-2">
229
+ <a href="/tags" class="px-3 py-1 text-xs rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors">
230
+ 全部标签
231
+ </a>
232
+ {allTags.slice(0, 10).map((tag) => (
233
+ <a href={`/tags/${tag.toLowerCase().replace(/\s+/g, '-')}`} class="px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
234
+ {tag}
235
+ </a>
236
+ ))}
237
+ {allTags.length > 10 && (
238
+ <a href="/tags" class="px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
239
+ +{allTags.length - 10} 更多
240
+ </a>
241
+ )}
242
+ </div>
243
+ </div>
244
+
245
+ <!-- 按分类筛选 -->
246
+ <div>
247
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">
248
+ 按分类筛选
249
+ </h3>
250
+ <div class="flex flex-wrap gap-2">
251
+ <a href="/categories" class="px-3 py-1 text-xs rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors">
252
+ 全部分类
253
+ </a>
254
+ {allCategories.slice(0, 8).map((category) => (
255
+ <a href={`/categories/${category.toLowerCase().replace(/\s+/g, '-')}`} class="px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
256
+ {category}
257
+ </a>
258
+ ))}
259
+ {allCategories.length > 8 && (
260
+ <a href="/categories" class="px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
261
+ +{allCategories.length - 8} 更多
262
+ </a>
263
+ )}
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- 文章列表区域 -->
270
+ <div class="mb-12">
271
+ <!-- 视图切换按钮 -->
272
+ <div class="flex items-center justify-end mb-4">
273
+ <div class="flex items-center gap-2 bg-slate-100 dark:bg-slate-700 p-1 rounded-lg">
274
+ <button
275
+ class="view-toggle-btn active px-3 py-1.5 text-sm rounded-md transition-colors"
276
+ data-view="card"
277
+ title="卡片模式"
278
+ >
279
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
280
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
281
+ </svg>
282
+ </button>
283
+ <button
284
+ class="view-toggle-btn px-3 py-1.5 text-sm rounded-md transition-colors"
285
+ data-view="list"
286
+ title="列表模式"
287
+ >
288
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
289
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
290
+ </svg>
291
+ </button>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- 卡片模式 -->
296
+ <div id="card-view" class="view-container">
297
+ {posts.length > 0 ? (
298
+ <div class="flex flex-wrap gap-6 mb-12">
299
+ {posts.map((post) => (
300
+ <div class="flex-shrink-0">
301
+ <PostCard post={post} layout="horizontal" />
302
+ </div>
303
+ ))}
304
+ </div>
305
+ ) : (
306
+ <div class="text-center py-16">
307
+ <div class="text-6xl mb-4">📝</div>
308
+ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
309
+ 暂无文章
310
+ </h3>
311
+ <p class="text-slate-600 dark:text-slate-400">
312
+ 目前还没有发布任何文章,请稍后再来查看。
313
+ </p>
314
+ </div>
315
+ )}
316
+ </div>
317
+
318
+ <!-- 列表模式 -->
319
+ <div id="list-view" class="view-container hidden">
320
+ {postTree.length > 0 ? (
321
+ <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-12">
322
+ <div class="post-tree">
323
+ {postTree.map((node) => (
324
+ <div class="tree-node">
325
+ {node.isFolder ? (
326
+ <div class="folder-node">
327
+ <button class="folder-toggle w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors border-b border-slate-100 dark:border-slate-700">
328
+ <svg class="folder-chevron w-4 h-4 text-slate-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
329
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
330
+ </svg>
331
+ <svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
332
+ <path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" />
333
+ </svg>
334
+ <span class="font-medium text-slate-900 dark:text-slate-100">{node.title || node.name}</span>
335
+ <span class="text-xs text-slate-400 ml-auto">{node.children.length} 篇</span>
336
+ </button>
337
+ <div class="folder-children hidden">
338
+ {node.children.map((child) => (
339
+ <div class="tree-node">
340
+ {child.isFolder ? (
341
+ <div class="folder-node ml-6">
342
+ <button class="folder-toggle w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors border-b border-slate-100 dark:border-slate-700">
343
+ <svg class="folder-chevron w-4 h-4 text-slate-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
344
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
345
+ </svg>
346
+ <svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
347
+ <path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" />
348
+ </svg>
349
+ <span class="font-medium text-slate-900 dark:text-slate-100">{child.title || child.name}</span>
350
+ <span class="text-xs text-slate-400 ml-auto">{child.children.length} 篇</span>
351
+ </button>
352
+ <div class="folder-children hidden">
353
+ {child.children.map((grandChild) => (
354
+ <a
355
+ href={`/posts/${grandChild.slug}`}
356
+ class="ml-12 flex items-start gap-4 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors border-b border-slate-100 dark:border-slate-700 group"
357
+ >
358
+ <svg class="w-4 h-4 text-slate-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
359
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
360
+ </svg>
361
+ <div class="flex-1 min-w-0">
362
+ <h4 class="font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors">{grandChild.title || grandChild.name}</h4>
363
+ {grandChild.description && (
364
+ <p class="text-sm text-slate-500 dark:text-slate-400 mt-1 line-clamp-1">{grandChild.description}</p>
365
+ )}
366
+ </div>
367
+ {grandChild.pubDate && (
368
+ <time class="text-xs text-slate-400 flex-shrink-0">{formatDate(grandChild.pubDate)}</time>
369
+ )}
370
+ </a>
371
+ ))}
372
+ </div>
373
+ </div>
374
+ ) : (
375
+ <a
376
+ href={`/posts/${child.slug}`}
377
+ class="ml-6 flex items-start gap-4 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors border-b border-slate-100 dark:border-slate-700 group"
378
+ >
379
+ <svg class="w-4 h-4 text-slate-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
380
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
381
+ </svg>
382
+ <div class="flex-1 min-w-0">
383
+ <h4 class="font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors">{child.title || child.name}</h4>
384
+ {child.description && (
385
+ <p class="text-sm text-slate-500 dark:text-slate-400 mt-1 line-clamp-1">{child.description}</p>
386
+ )}
387
+ </div>
388
+ {child.pubDate && (
389
+ <time class="text-xs text-slate-400 flex-shrink-0">{formatDate(child.pubDate)}</time>
390
+ )}
391
+ </a>
392
+ )}
393
+ </div>
394
+ ))}
395
+ </div>
396
+ </div>
397
+ ) : (
398
+ <a
399
+ href={`/posts/${node.slug}`}
400
+ class="flex items-start gap-4 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors border-b border-slate-100 dark:border-slate-700 group"
401
+ >
402
+ <svg class="w-4 h-4 text-slate-400 mt-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
403
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
404
+ </svg>
405
+ <div class="flex-1 min-w-0">
406
+ <h4 class="font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors">{node.title || node.name}</h4>
407
+ {node.description && (
408
+ <p class="text-sm text-slate-500 dark:text-slate-400 mt-1 line-clamp-1">{node.description}</p>
409
+ )}
410
+ </div>
411
+ {node.pubDate && (
412
+ <time class="text-xs text-slate-400 flex-shrink-0">{formatDate(node.pubDate)}</time>
413
+ )}
414
+ </a>
415
+ )}
416
+ </div>
417
+ ))}
418
+ </div>
419
+ </div>
420
+ ) : (
421
+ <div class="text-center py-16">
422
+ <div class="text-6xl mb-4">📝</div>
423
+ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
424
+ 暂无文章
425
+ </h3>
426
+ <p class="text-slate-600 dark:text-slate-400">
427
+ 目前还没有发布任何文章,请稍后再来查看。
428
+ </p>
429
+ </div>
430
+ )}
431
+ </div>
432
+ </div>
433
+
434
+ <!-- 分页导航(仅卡片模式显示) -->
435
+ <div id="pagination-container">
436
+ {totalPages > 1 && (
437
+ <Pagination
438
+ currentPage={currentPage}
439
+ totalPages={totalPages}
440
+ baseUrl="/posts"
441
+ />
442
+ )}
443
+ </div>
444
+ </PageLayout>
445
+
446
+ <style>
447
+ .line-clamp-1 {
448
+ display: -webkit-box;
449
+ -webkit-line-clamp: 1;
450
+ -webkit-box-orient: vertical;
451
+ overflow: hidden;
452
+ }
453
+
454
+ .view-toggle-btn {
455
+ @apply text-slate-500 dark:text-slate-400;
456
+ }
457
+
458
+ .view-toggle-btn.active {
459
+ @apply bg-white dark:bg-slate-600 text-primary-600 dark:text-primary-400 shadow-sm;
460
+ }
461
+
462
+ .view-toggle-btn:not(.active):hover {
463
+ @apply text-slate-700 dark:text-slate-200;
464
+ }
465
+
466
+ .folder-toggle.expanded .folder-chevron {
467
+ transform: rotate(90deg);
468
+ }
469
+
470
+ .folder-children.show {
471
+ display: block;
472
+ }
473
+ </style>
474
+
475
+ <script>
476
+ function initViewToggle() {
477
+ const toggleBtns = document.querySelectorAll('.view-toggle-btn');
478
+ const cardView = document.getElementById('card-view');
479
+ const listView = document.getElementById('list-view');
480
+ const pagination = document.getElementById('pagination-container');
481
+
482
+ // 从 localStorage 恢复视图模式
483
+ const savedView = localStorage.getItem('posts-view-mode') || 'card';
484
+ setView(savedView);
485
+
486
+ toggleBtns.forEach(btn => {
487
+ btn.addEventListener('click', () => {
488
+ const view = btn.getAttribute('data-view');
489
+ if (view) {
490
+ setView(view);
491
+ localStorage.setItem('posts-view-mode', view);
492
+ }
493
+ });
494
+ });
495
+
496
+ function setView(view: string) {
497
+ toggleBtns.forEach(btn => {
498
+ btn.classList.toggle('active', btn.getAttribute('data-view') === view);
499
+ });
500
+
501
+ if (cardView && listView && pagination) {
502
+ if (view === 'card') {
503
+ cardView.classList.remove('hidden');
504
+ listView.classList.add('hidden');
505
+ pagination.classList.remove('hidden');
506
+ } else {
507
+ cardView.classList.add('hidden');
508
+ listView.classList.remove('hidden');
509
+ pagination.classList.add('hidden');
510
+ }
511
+ }
512
+ }
513
+
514
+ // 文件夹展开/收起
515
+ const folderToggles = document.querySelectorAll('.folder-toggle');
516
+ folderToggles.forEach(toggle => {
517
+ toggle.addEventListener('click', () => {
518
+ const children = toggle.nextElementSibling;
519
+ const isExpanded = toggle.classList.toggle('expanded');
520
+
521
+ if (children && children.classList.contains('folder-children')) {
522
+ children.classList.toggle('show', isExpanded);
523
+ }
524
+ });
525
+ });
526
+
527
+ // 默认展开第一层文件夹
528
+ const firstLevelToggles = document.querySelectorAll('.post-tree > .tree-node > .folder-node > .folder-toggle');
529
+ firstLevelToggles.forEach(toggle => {
530
+ if (!toggle.classList.contains('expanded')) {
531
+ (toggle as HTMLElement).click();
532
+ }
533
+ });
534
+ }
535
+
536
+ if (document.readyState === 'loading') {
537
+ document.addEventListener('DOMContentLoaded', initViewToggle);
538
+ } else {
539
+ initViewToggle();
540
+ }
541
+ </script>
@@ -0,0 +1,146 @@
1
+ ---
2
+ import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
3
+ import PostCard from '@jet-w/astro-blog/components/blog/PostCard.astro';
4
+ import Pagination from '@jet-w/astro-blog/components/ui/Pagination.astro';
5
+ import { getCollection } from 'astro:content';
6
+
7
+ export async function getStaticPaths() {
8
+ const allPostsCollection = await getCollection('posts');
9
+ const publishedPosts = allPostsCollection.filter(post => !post.data.draft);
10
+
11
+ const postsPerPage = 10;
12
+ const totalPages = Math.ceil(publishedPosts.length / postsPerPage);
13
+
14
+ // 生成从第2页开始的所有页面路径(第1页由 index.astro 处理)
15
+ return Array.from({ length: totalPages - 1 }, (_, i) => ({
16
+ params: { page: String(i + 2) },
17
+ }));
18
+ }
19
+
20
+ const { page } = Astro.params;
21
+ const currentPage = parseInt(page as string, 10);
22
+
23
+ // 从新的内容目录获取文章数据
24
+ const allPostsCollection = await getCollection('posts');
25
+ const publishedPosts = allPostsCollection
26
+ .filter(post => !post.data.draft)
27
+ .sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
28
+
29
+ const allPosts = publishedPosts.map(post => ({
30
+ slug: post.id.toLowerCase(),
31
+ title: post.data.title,
32
+ description: post.data.description,
33
+ pubDate: post.data.pubDate,
34
+ tags: post.data.tags,
35
+ categories: post.data.categories,
36
+ author: post.data.author,
37
+ readingTime: 5,
38
+ image: post.data.image
39
+ }));
40
+
41
+ // 分页逻辑
42
+ const postsPerPage = 10;
43
+ const totalPosts = allPosts.length;
44
+ const totalPages = Math.ceil(totalPosts / postsPerPage);
45
+ const startIndex = (currentPage - 1) * postsPerPage;
46
+ const endIndex = startIndex + postsPerPage;
47
+ const posts = allPosts.slice(startIndex, endIndex);
48
+
49
+ // 获取所有标签和分类用于筛选
50
+ const allTags = [...new Set(allPosts.flatMap(post => post.tags || []))];
51
+ const allCategories = [...new Set(allPosts.flatMap(post => post.categories || []))];
52
+ ---
53
+
54
+ <PageLayout
55
+ title={`文章列表 - 第 ${currentPage} 页`}
56
+ description="浏览所有技术文章和分享内容"
57
+ showSidebar={true}
58
+ >
59
+ <!-- 页面头部 -->
60
+ <div class="mb-12">
61
+ <div class="text-center">
62
+ <h1 class="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
63
+ 文章列表
64
+ </h1>
65
+ <p class="text-xl text-slate-600 dark:text-slate-400 mb-8">
66
+ 分享技术思考和学习心得
67
+ </p>
68
+
69
+ <!-- 统计信息 -->
70
+ <div class="inline-flex items-center space-x-6 text-sm text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-800 px-6 py-3 rounded-lg">
71
+ <span>共 {totalPosts} 篇文章</span>
72
+ <span>•</span>
73
+ <span>{allTags.length} 个标签</span>
74
+ <span>•</span>
75
+ <span>{allCategories.length} 个分类</span>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- 筛选器 -->
81
+ <div class="mb-8 p-6 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
82
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
83
+ <!-- 按标签筛选 -->
84
+ <div>
85
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">
86
+ 按标签筛选
87
+ </h3>
88
+ <div class="flex flex-wrap gap-2">
89
+ <button class="px-3 py-1 text-xs rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors">
90
+ 全部
91
+ </button>
92
+ {allTags.map((tag) => (
93
+ <button class="px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
94
+ {tag}
95
+ </button>
96
+ ))}
97
+ </div>
98
+ </div>
99
+
100
+ <!-- 按分类筛选 -->
101
+ <div>
102
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">
103
+ 按分类筛选
104
+ </h3>
105
+ <div class="flex flex-wrap gap-2">
106
+ <button class="px-3 py-1 text-xs rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors">
107
+ 全部
108
+ </button>
109
+ {allCategories.map((category) => (
110
+ <button class="px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
111
+ {category}
112
+ </button>
113
+ ))}
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- 文章列表 -->
120
+ {posts.length > 0 ? (
121
+ <div class="space-y-8 mb-12">
122
+ {posts.map((post) => (
123
+ <PostCard post={post} layout="horizontal" />
124
+ ))}
125
+ </div>
126
+ ) : (
127
+ <div class="text-center py-16">
128
+ <div class="text-6xl mb-4">📝</div>
129
+ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
130
+ 暂无文章
131
+ </h3>
132
+ <p class="text-slate-600 dark:text-slate-400">
133
+ 目前还没有发布任何文章,请稍后再来查看。
134
+ </p>
135
+ </div>
136
+ )}
137
+
138
+ <!-- 分页导航 -->
139
+ {totalPages > 1 && (
140
+ <Pagination
141
+ currentPage={currentPage}
142
+ totalPages={totalPages}
143
+ baseUrl="/posts"
144
+ />
145
+ )}
146
+ </PageLayout>