@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,147 @@
1
+ ---
2
+ import { siteConfig } from '../../config/site';
3
+ ---
4
+
5
+ <section class="py-16 mb-16">
6
+ <div class="text-center">
7
+ <!-- 头像 -->
8
+ {siteConfig.avatar && (
9
+ <div class="mb-8">
10
+ <img
11
+ src={siteConfig.avatar}
12
+ alt={siteConfig.author}
13
+ class="w-32 h-32 rounded-full mx-auto shadow-lg"
14
+ />
15
+ </div>
16
+ )}
17
+
18
+ <!-- 标题和描述 -->
19
+ <h1 class="text-4xl md:text-5xl font-bold text-slate-900 dark:text-slate-100 mb-6">
20
+ {siteConfig.title}
21
+ </h1>
22
+
23
+ <p class="text-xl text-slate-600 dark:text-slate-400 mb-8 max-w-2xl mx-auto leading-relaxed">
24
+ {siteConfig.description}
25
+ </p>
26
+
27
+ <!-- 社交链接 -->
28
+ {siteConfig.social && (
29
+ <div class="flex justify-center space-x-6 mb-12">
30
+ {siteConfig.social.github && (
31
+ <a
32
+ href={siteConfig.social.github}
33
+ target="_blank"
34
+ rel="noopener noreferrer"
35
+ class="text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
36
+ aria-label="GitHub"
37
+ >
38
+ <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
39
+ <path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/>
40
+ </svg>
41
+ </a>
42
+ )}
43
+
44
+ {siteConfig.social.twitter && (
45
+ <a
46
+ href={siteConfig.social.twitter}
47
+ target="_blank"
48
+ rel="noopener noreferrer"
49
+ class="text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
50
+ aria-label="Twitter"
51
+ >
52
+ <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
53
+ <path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
54
+ </svg>
55
+ </a>
56
+ )}
57
+
58
+ {siteConfig.social.linkedin && (
59
+ <a
60
+ href={siteConfig.social.linkedin}
61
+ target="_blank"
62
+ rel="noopener noreferrer"
63
+ class="text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
64
+ aria-label="LinkedIn"
65
+ >
66
+ <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
67
+ <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
68
+ </svg>
69
+ </a>
70
+ )}
71
+
72
+ {siteConfig.social.email && (
73
+ <a
74
+ href={siteConfig.social.email}
75
+ class="text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
76
+ aria-label="Email"
77
+ >
78
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
79
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
80
+ </svg>
81
+ </a>
82
+ )}
83
+ </div>
84
+ )}
85
+
86
+ <!-- 行动按钮 -->
87
+ <div class="flex flex-col sm:flex-row gap-4 justify-center">
88
+ <a
89
+ href="/posts"
90
+ class="btn-primary inline-flex items-center space-x-2"
91
+ >
92
+ <span>浏览文章</span>
93
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
95
+ </svg>
96
+ </a>
97
+
98
+ <a
99
+ href="/about"
100
+ class="btn-secondary inline-flex items-center space-x-2"
101
+ >
102
+ <span>关于我</span>
103
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
104
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
105
+ </svg>
106
+ </a>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- 装饰性背景 -->
111
+ <div class="absolute top-0 left-1/2 transform -translate-x-1/2 w-full max-w-4xl h-96 -z-10 opacity-20">
112
+ <div class="relative w-full h-full">
113
+ <div class="absolute top-20 left-10 w-20 h-20 bg-primary-300 rounded-full mix-blend-multiply filter blur-xl animate-blob"></div>
114
+ <div class="absolute top-40 right-10 w-20 h-20 bg-accent-300 rounded-full mix-blend-multiply filter blur-xl animate-blob animation-delay-2000"></div>
115
+ <div class="absolute bottom-20 left-1/2 w-20 h-20 bg-secondary-300 rounded-full mix-blend-multiply filter blur-xl animate-blob animation-delay-4000"></div>
116
+ </div>
117
+ </div>
118
+ </section>
119
+
120
+ <style>
121
+ @keyframes blob {
122
+ 0% {
123
+ transform: translate(0px, 0px) scale(1);
124
+ }
125
+ 33% {
126
+ transform: translate(30px, -50px) scale(1.1);
127
+ }
128
+ 66% {
129
+ transform: translate(-20px, 20px) scale(0.9);
130
+ }
131
+ 100% {
132
+ transform: translate(0px, 0px) scale(1);
133
+ }
134
+ }
135
+
136
+ .animate-blob {
137
+ animation: blob 7s infinite;
138
+ }
139
+
140
+ .animation-delay-2000 {
141
+ animation-delay: 2s;
142
+ }
143
+
144
+ .animation-delay-4000 {
145
+ animation-delay: 4s;
146
+ }
147
+ </style>
@@ -0,0 +1,245 @@
1
+ <template>
2
+ <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
3
+ <!-- Tab 头部 -->
4
+ <div class="flex border-b border-slate-200 dark:border-slate-700">
5
+ <button
6
+ v-for="tab in tabs"
7
+ :key="tab.id"
8
+ @click="activeTab = tab.id"
9
+ class="flex-1 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium transition-colors"
10
+ :class="activeTab === tab.id
11
+ ? 'text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border-b-2 border-primary-500'
12
+ : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700/50'"
13
+ >
14
+ <component :is="tab.icon" class="w-4 h-4" />
15
+ <span>{{ tab.name }}</span>
16
+ <span class="text-xs px-1.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400">
17
+ {{ tab.count }}
18
+ </span>
19
+ </button>
20
+ </div>
21
+
22
+ <!-- Tab 内容 -->
23
+ <div class="p-4 max-h-80 overflow-y-auto">
24
+ <!-- 标签 Tab -->
25
+ <div v-if="activeTab === 'tags'" class="flex flex-wrap gap-2">
26
+ <a
27
+ v-for="tag in tags"
28
+ :key="tag.name"
29
+ :href="`/tags/${encodeTag(tag.name)}`"
30
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm transition-colors"
31
+ :class="getTagColorClass(tag.count)"
32
+ >
33
+ <span>#{{ tag.name }}</span>
34
+ <span class="text-xs opacity-70">({{ tag.count }})</span>
35
+ </a>
36
+ </div>
37
+
38
+ <!-- 归档 Tab -->
39
+ <div v-if="activeTab === 'archives'" class="space-y-3">
40
+ <a
41
+ v-for="archive in archives"
42
+ :key="archive.key"
43
+ :href="`/archives/${archive.year}/${archive.month}`"
44
+ class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
45
+ >
46
+ <div class="flex items-center gap-3">
47
+ <svg class="w-4 h-4 text-slate-400 group-hover:text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
49
+ </svg>
50
+ <span class="text-slate-700 dark:text-slate-300 group-hover:text-primary-600 dark:group-hover:text-primary-400">
51
+ {{ archive.year }}年{{ archive.month }}月
52
+ </span>
53
+ </div>
54
+ <span class="text-xs text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded-full">
55
+ {{ archive.count }}篇
56
+ </span>
57
+ </a>
58
+ </div>
59
+
60
+ <!-- 分类 Tab -->
61
+ <div v-if="activeTab === 'categories'" class="space-y-2">
62
+ <a
63
+ v-for="category in categories"
64
+ :key="category.name"
65
+ :href="`/categories/${encodeCategory(category.name)}`"
66
+ class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
67
+ >
68
+ <div class="flex items-center gap-3">
69
+ <svg class="w-4 h-4 text-slate-400 group-hover:text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
70
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
71
+ </svg>
72
+ <span class="text-slate-700 dark:text-slate-300 group-hover:text-primary-600 dark:group-hover:text-primary-400">
73
+ {{ category.name }}
74
+ </span>
75
+ </div>
76
+ <span class="text-xs text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded-full">
77
+ {{ category.count }}篇
78
+ </span>
79
+ </a>
80
+ </div>
81
+
82
+ <!-- 时间轴 Tab -->
83
+ <div v-if="activeTab === 'timeline'" class="relative">
84
+ <div class="absolute left-4 top-0 bottom-0 w-0.5 bg-slate-200 dark:bg-slate-700"></div>
85
+ <div class="space-y-4">
86
+ <div v-for="item in timeline" :key="item.slug" class="relative pl-10">
87
+ <div class="absolute left-2.5 w-3 h-3 rounded-full bg-primary-500 border-2 border-white dark:border-slate-800"></div>
88
+ <a
89
+ :href="`/posts/${item.slug}`"
90
+ class="block p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
91
+ >
92
+ <div class="text-xs text-slate-400 mb-1">{{ formatDate(item.pubDate) }}</div>
93
+ <div class="text-sm font-medium text-slate-700 dark:text-slate-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 line-clamp-1">
94
+ {{ item.title }}
95
+ </div>
96
+ </a>
97
+ </div>
98
+ </div>
99
+ <a
100
+ href="/archives"
101
+ class="mt-4 flex items-center justify-center gap-2 py-2 text-sm text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
102
+ >
103
+ <span>查看全部时间轴</span>
104
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
106
+ </svg>
107
+ </a>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </template>
112
+
113
+ <script setup lang="ts">
114
+ import { ref, computed, h } from 'vue'
115
+
116
+ interface TagItem {
117
+ name: string
118
+ count: number
119
+ }
120
+
121
+ interface ArchiveItem {
122
+ year: string
123
+ month: string
124
+ key: string
125
+ count: number
126
+ }
127
+
128
+ interface CategoryItem {
129
+ name: string
130
+ count: number
131
+ }
132
+
133
+ interface TimelineItem {
134
+ slug: string
135
+ title: string
136
+ pubDate: string
137
+ }
138
+
139
+ interface Props {
140
+ tags: TagItem[]
141
+ archives: ArchiveItem[]
142
+ categories: CategoryItem[]
143
+ timeline: TimelineItem[]
144
+ }
145
+
146
+ const props = defineProps<Props>()
147
+
148
+ const activeTab = ref('tags')
149
+
150
+ // 图标组件
151
+ const TagIcon = {
152
+ render() {
153
+ return h('svg', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [
154
+ h('path', {
155
+ 'stroke-linecap': 'round',
156
+ 'stroke-linejoin': 'round',
157
+ 'stroke-width': '2',
158
+ d: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z'
159
+ })
160
+ ])
161
+ }
162
+ }
163
+
164
+ const ArchiveIcon = {
165
+ render() {
166
+ return h('svg', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [
167
+ h('path', {
168
+ 'stroke-linecap': 'round',
169
+ 'stroke-linejoin': 'round',
170
+ 'stroke-width': '2',
171
+ d: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4'
172
+ })
173
+ ])
174
+ }
175
+ }
176
+
177
+ const CategoryIcon = {
178
+ render() {
179
+ return h('svg', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [
180
+ h('path', {
181
+ 'stroke-linecap': 'round',
182
+ 'stroke-linejoin': 'round',
183
+ 'stroke-width': '2',
184
+ d: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z'
185
+ })
186
+ ])
187
+ }
188
+ }
189
+
190
+ const TimelineIcon = {
191
+ render() {
192
+ return h('svg', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [
193
+ h('path', {
194
+ 'stroke-linecap': 'round',
195
+ 'stroke-linejoin': 'round',
196
+ 'stroke-width': '2',
197
+ d: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'
198
+ })
199
+ ])
200
+ }
201
+ }
202
+
203
+ const tabs = computed(() => [
204
+ { id: 'tags', name: '标签', count: props.tags.length, icon: TagIcon },
205
+ { id: 'archives', name: '归档', count: props.archives.length, icon: ArchiveIcon },
206
+ { id: 'categories', name: '分类', count: props.categories.length, icon: CategoryIcon },
207
+ { id: 'timeline', name: '时间轴', count: props.timeline.length, icon: TimelineIcon },
208
+ ])
209
+
210
+ const encodeTag = (tag: string) => {
211
+ return encodeURIComponent(tag.toLowerCase().replace(/\s+/g, '-'))
212
+ }
213
+
214
+ const encodeCategory = (category: string) => {
215
+ return encodeURIComponent(category.toLowerCase().replace(/\s+/g, '-'))
216
+ }
217
+
218
+ const getTagColorClass = (count: number) => {
219
+ if (count >= 10) {
220
+ return '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'
221
+ } else if (count >= 5) {
222
+ return 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900/50'
223
+ } else {
224
+ return 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
225
+ }
226
+ }
227
+
228
+ const formatDate = (dateStr: string) => {
229
+ const date = new Date(dateStr)
230
+ return new Intl.DateTimeFormat('zh-CN', {
231
+ year: 'numeric',
232
+ month: '2-digit',
233
+ day: '2-digit'
234
+ }).format(date)
235
+ }
236
+ </script>
237
+
238
+ <style scoped>
239
+ .line-clamp-1 {
240
+ display: -webkit-box;
241
+ -webkit-line-clamp: 1;
242
+ -webkit-box-orient: vertical;
243
+ overflow: hidden;
244
+ }
245
+ </style>
@@ -0,0 +1,161 @@
1
+ ---
2
+ export interface Props {
3
+ post: {
4
+ slug: string;
5
+ title: string;
6
+ description?: string;
7
+ pubDate?: Date;
8
+ tags?: string[];
9
+ categories?: string[];
10
+ author?: string;
11
+ readingTime?: number;
12
+ image?: string;
13
+ };
14
+ featured?: boolean;
15
+ layout?: 'vertical' | 'horizontal';
16
+ }
17
+
18
+ const { post, featured = false, layout = 'vertical' } = Astro.props;
19
+
20
+ const formattedDate = post.pubDate
21
+ ? new Intl.DateTimeFormat('zh-CN', {
22
+ year: 'numeric',
23
+ month: 'long',
24
+ day: 'numeric'
25
+ }).format(post.pubDate)
26
+ : null;
27
+
28
+ const isHorizontal = layout === 'horizontal';
29
+
30
+ // 将标签名转换为 slug 格式
31
+ const tagToSlug = (tag: string) => tag.toLowerCase().replace(/\s+/g, '-');
32
+ // 将分类名转换为 slug 格式
33
+ const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+/g, '-');
34
+ ---
35
+
36
+ <article class={`group card hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 ${isHorizontal ? 'flex gap-6 items-center' : 'block'}`}>
37
+ <!-- 文章图片 -->
38
+ {post.image && (
39
+ <a href={`/posts/${post.slug}`} class={`block overflow-hidden rounded-lg ${
40
+ isHorizontal ? 'w-48 h-32 flex-shrink-0' : 'w-full h-48 mb-4'
41
+ }`}>
42
+ <img
43
+ src={post.image}
44
+ alt={post.title}
45
+ class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
46
+ loading="lazy"
47
+ />
48
+ </a>
49
+ )}
50
+
51
+ <!-- 文章内容 -->
52
+ <div class={`${isHorizontal ? 'flex-1' : ''}`}>
53
+ <!-- 分类 -->
54
+ {post.categories && post.categories.length > 0 && (
55
+ <div class="flex flex-wrap gap-2 mb-2">
56
+ {post.categories.slice(0, 2).map((category) => (
57
+ <a
58
+ href={`/categories/${categoryToSlug(category)}`}
59
+ class="text-xs px-2 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-full hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors"
60
+ onclick="event.stopPropagation();"
61
+ >
62
+ {category}
63
+ </a>
64
+ ))}
65
+ </div>
66
+ )}
67
+
68
+ <!-- 标签 -->
69
+ {post.tags && post.tags.length > 0 && (
70
+ <div class="flex flex-wrap gap-2 mb-3">
71
+ {post.tags.slice(0, 3).map((tag) => (
72
+ <a
73
+ href={`/tags/${tagToSlug(tag)}`}
74
+ class="text-xs px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors"
75
+ onclick="event.stopPropagation();"
76
+ >
77
+ {tag}
78
+ </a>
79
+ ))}
80
+ {post.tags.length > 3 && (
81
+ <span class="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded-full">
82
+ +{post.tags.length - 3}
83
+ </span>
84
+ )}
85
+ </div>
86
+ )}
87
+
88
+ <!-- 标题 -->
89
+ <a href={`/posts/${post.slug}`}>
90
+ <h3 class={`font-bold text-slate-900 dark:text-slate-100 hover:text-primary-500 transition-colors line-clamp-2 mb-3 ${
91
+ featured ? 'text-xl' : isHorizontal ? 'text-lg' : 'text-lg'
92
+ }`}>
93
+ {post.title}
94
+ </h3>
95
+ </a>
96
+
97
+ <!-- 描述 -->
98
+ <a href={`/posts/${post.slug}`}>
99
+ <p class={`text-slate-600 dark:text-slate-400 line-clamp-3 mb-4 hover:text-slate-700 dark:hover:text-slate-300 transition-colors ${
100
+ isHorizontal ? 'text-sm' : 'text-base'
101
+ }`}>
102
+ {post.description}
103
+ </p>
104
+ </a>
105
+
106
+ <!-- 元信息 -->
107
+ <div class="flex items-center justify-between text-sm text-slate-500 dark:text-slate-400">
108
+ <div class="flex items-center space-x-4">
109
+ {post.author && (
110
+ <span class="flex items-center space-x-1">
111
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
113
+ </svg>
114
+ <span>{post.author}</span>
115
+ </span>
116
+ )}
117
+
118
+ {post.pubDate && formattedDate && (
119
+ <time class="flex items-center space-x-1" datetime={post.pubDate.toISOString()}>
120
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
122
+ </svg>
123
+ <span>{formattedDate}</span>
124
+ </time>
125
+ )}
126
+
127
+ {post.readingTime && (
128
+ <span class="flex items-center space-x-1">
129
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
131
+ </svg>
132
+ <span>{post.readingTime}分钟</span>
133
+ </span>
134
+ )}
135
+ </div>
136
+
137
+ <a href={`/posts/${post.slug}`} class="flex items-center text-primary-500 hover:text-primary-600 transition-colors">
138
+ <span class="text-sm font-medium">阅读更多</span>
139
+ <svg class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
140
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
141
+ </svg>
142
+ </a>
143
+ </div>
144
+ </div>
145
+ </article>
146
+
147
+ <style>
148
+ .line-clamp-2 {
149
+ display: -webkit-box;
150
+ -webkit-line-clamp: 2;
151
+ -webkit-box-orient: vertical;
152
+ overflow: hidden;
153
+ }
154
+
155
+ .line-clamp-3 {
156
+ display: -webkit-box;
157
+ -webkit-line-clamp: 3;
158
+ -webkit-box-orient: vertical;
159
+ overflow: hidden;
160
+ }
161
+ </style>
@@ -0,0 +1,106 @@
1
+ ---
2
+ // 模拟获取上一篇和下一篇文章
3
+ const allPosts = [
4
+ {
5
+ slug: 'tailwind-best-practices',
6
+ title: 'Tailwind CSS最佳实践指南',
7
+ pubDate: new Date('2025-01-06')
8
+ },
9
+ {
10
+ slug: 'welcome-to-astro',
11
+ title: '欢迎来到Astro技术博客',
12
+ pubDate: new Date('2025-01-08')
13
+ },
14
+ {
15
+ slug: 'astro-vs-nextjs',
16
+ title: 'Astro vs Next.js:静态站点生成器的对比',
17
+ pubDate: new Date('2025-01-09')
18
+ }
19
+ ];
20
+
21
+ // 当前文章(这里硬编码,实际应该从props或context获取)
22
+ const currentSlug = 'welcome-to-astro';
23
+ const currentIndex = allPosts.findIndex(post => post.slug === currentSlug);
24
+
25
+ const prevPost = currentIndex > 0 ? allPosts[currentIndex - 1] : null;
26
+ const nextPost = currentIndex < allPosts.length - 1 ? allPosts[currentIndex + 1] : null;
27
+ ---
28
+
29
+ {(prevPost || nextPost) && (
30
+ <nav class="flex flex-col sm:flex-row justify-between items-stretch gap-4 my-12">
31
+ <!-- 上一篇文章 -->
32
+ <div class="flex-1">
33
+ {prevPost ? (
34
+ <a
35
+ href={`/posts/${prevPost.slug}`}
36
+ class="group flex flex-col sm:flex-row items-start sm:items-center p-6 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 hover:shadow-md transition-all duration-300 h-full"
37
+ >
38
+ <div class="flex items-center text-slate-500 dark:text-slate-400 mb-2 sm:mb-0 sm:mr-4">
39
+ <svg class="w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
41
+ </svg>
42
+ <span class="text-sm font-medium">上一篇</span>
43
+ </div>
44
+ <div class="flex-1 min-w-0">
45
+ <h3 class="text-slate-900 dark:text-slate-100 font-medium group-hover:text-primary-500 transition-colors line-clamp-2">
46
+ {prevPost.title}
47
+ </h3>
48
+ </div>
49
+ </a>
50
+ ) : (
51
+ <div class="p-6 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 opacity-50 h-full">
52
+ <div class="flex items-center text-slate-400 dark:text-slate-600 mb-2">
53
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
55
+ </svg>
56
+ <span class="text-sm font-medium">没有更多了</span>
57
+ </div>
58
+ </div>
59
+ )}
60
+ </div>
61
+
62
+ <!-- 分隔线 -->
63
+ <div class="hidden sm:block w-px bg-slate-200 dark:bg-slate-700 mx-4"></div>
64
+ <div class="sm:hidden h-px bg-slate-200 dark:bg-slate-700"></div>
65
+
66
+ <!-- 下一篇文章 -->
67
+ <div class="flex-1">
68
+ {nextPost ? (
69
+ <a
70
+ href={`/posts/${nextPost.slug}`}
71
+ class="group flex flex-col sm:flex-row-reverse items-start sm:items-center p-6 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 hover:shadow-md transition-all duration-300 h-full text-left sm:text-right"
72
+ >
73
+ <div class="flex items-center text-slate-500 dark:text-slate-400 mb-2 sm:mb-0 sm:ml-4">
74
+ <span class="text-sm font-medium mr-2">下一篇</span>
75
+ <svg class="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
76
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
77
+ </svg>
78
+ </div>
79
+ <div class="flex-1 min-w-0">
80
+ <h3 class="text-slate-900 dark:text-slate-100 font-medium group-hover:text-primary-500 transition-colors line-clamp-2">
81
+ {nextPost.title}
82
+ </h3>
83
+ </div>
84
+ </a>
85
+ ) : (
86
+ <div class="p-6 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 opacity-50 h-full text-right">
87
+ <div class="flex items-center justify-end text-slate-400 dark:text-slate-600 mb-2">
88
+ <span class="text-sm font-medium mr-2">没有更多了</span>
89
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
90
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
91
+ </svg>
92
+ </div>
93
+ </div>
94
+ )}
95
+ </div>
96
+ </nav>
97
+ )}
98
+
99
+ <style>
100
+ .line-clamp-2 {
101
+ display: -webkit-box;
102
+ -webkit-line-clamp: 2;
103
+ -webkit-box-orient: vertical;
104
+ overflow: hidden;
105
+ }
106
+ </style>