@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,118 @@
1
+ <template>
2
+ <div ref="chartContainer" :style="containerStyle" class="echarts-container"></div>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
7
+ import * as echarts from 'echarts'
8
+
9
+ const props = defineProps({
10
+ // 图表配置项
11
+ option: {
12
+ type: Object,
13
+ required: true
14
+ },
15
+ // 图表宽度
16
+ width: {
17
+ type: String,
18
+ default: '100%'
19
+ },
20
+ // 图表高度
21
+ height: {
22
+ type: String,
23
+ default: '400px'
24
+ },
25
+ // 主题:'light' | 'dark'
26
+ theme: {
27
+ type: String,
28
+ default: 'light'
29
+ },
30
+ // 是否自动调整大小
31
+ autoResize: {
32
+ type: Boolean,
33
+ default: true
34
+ }
35
+ })
36
+
37
+ const chartContainer = ref<HTMLDivElement>()
38
+ let chartInstance: echarts.ECharts | null = null
39
+
40
+ const containerStyle = computed(() => ({
41
+ width: props.width,
42
+ height: props.height
43
+ }))
44
+
45
+ // 初始化图表
46
+ const initChart = () => {
47
+ if (!chartContainer.value) {
48
+ console.error('[ECharts] Container not found')
49
+ return
50
+ }
51
+
52
+ // 如果已存在实例,先销毁
53
+ if (chartInstance) {
54
+ chartInstance.dispose()
55
+ }
56
+
57
+ try {
58
+ // 创建新实例
59
+ chartInstance = echarts.init(chartContainer.value, props.theme)
60
+ chartInstance.setOption(props.option)
61
+ console.log('[ECharts] Chart initialized successfully')
62
+ } catch (error) {
63
+ console.error('[ECharts] Error initializing chart:', error)
64
+ }
65
+ }
66
+
67
+ // 更新图表配置
68
+ const updateChart = () => {
69
+ if (chartInstance) {
70
+ chartInstance.setOption(props.option, true)
71
+ }
72
+ }
73
+
74
+ // 调整图表大小
75
+ const handleResize = () => {
76
+ if (chartInstance) {
77
+ chartInstance.resize()
78
+ }
79
+ }
80
+
81
+ onMounted(async () => {
82
+ await nextTick()
83
+ initChart()
84
+
85
+ if (props.autoResize) {
86
+ window.addEventListener('resize', handleResize)
87
+ }
88
+ })
89
+
90
+ onUnmounted(() => {
91
+ if (props.autoResize) {
92
+ window.removeEventListener('resize', handleResize)
93
+ }
94
+
95
+ if (chartInstance) {
96
+ chartInstance.dispose()
97
+ chartInstance = null
98
+ }
99
+ })
100
+
101
+ // 监听配置变化
102
+ watch(() => props.option, updateChart, { deep: true })
103
+
104
+ // 监听主题变化
105
+ watch(() => props.theme, initChart)
106
+
107
+ // 暴露方法供外部调用
108
+ defineExpose({
109
+ getInstance: () => chartInstance,
110
+ resize: handleResize
111
+ })
112
+ </script>
113
+
114
+ <style scoped>
115
+ .echarts-container {
116
+ min-height: 200px;
117
+ }
118
+ </style>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div ref="mermaidContainer" class="mermaid-container"></div>
3
+ </template>
4
+
5
+ <script setup>
6
+ import { ref, onMounted, watch } from 'vue'
7
+ import mermaid from 'mermaid'
8
+
9
+ const props = defineProps({
10
+ chart: {
11
+ type: String,
12
+ required: true
13
+ }
14
+ })
15
+
16
+ const mermaidContainer = ref()
17
+
18
+ // 配置mermaid
19
+ mermaid.initialize({
20
+ startOnLoad: false,
21
+ theme: 'default',
22
+ securityLevel: 'loose',
23
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif'
24
+ })
25
+
26
+ const renderChart = async () => {
27
+ if (!mermaidContainer.value) return
28
+
29
+ try {
30
+ // 清空容器
31
+ mermaidContainer.value.innerHTML = ''
32
+
33
+ // 生成唯一ID
34
+ const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`
35
+
36
+ // 渲染图表
37
+ const { svg } = await mermaid.render(id, props.chart)
38
+ mermaidContainer.value.innerHTML = svg
39
+ } catch (error) {
40
+ console.error('Mermaid rendering error:', error)
41
+ mermaidContainer.value.innerHTML = `
42
+ <div class="text-red-600 border border-red-300 rounded p-4">
43
+ <h4 class="font-semibold">Mermaid渲染错误</h4>
44
+ <pre class="text-sm mt-2">${error.message}</pre>
45
+ </div>
46
+ `
47
+ }
48
+ }
49
+
50
+ onMounted(() => {
51
+ renderChart()
52
+ })
53
+
54
+ // 监听chart属性变化
55
+ watch(() => props.chart, () => {
56
+ renderChart()
57
+ })
58
+ </script>
59
+
60
+ <style scoped>
61
+ .mermaid-container {
62
+ @apply flex justify-center my-4;
63
+ }
64
+
65
+ .mermaid-container :deep(svg) {
66
+ @apply max-w-full h-auto;
67
+ }
68
+
69
+ /* 深色主题适配 */
70
+ .dark .mermaid-container :deep(svg) {
71
+ @apply invert;
72
+ }
73
+ </style>
@@ -0,0 +1,27 @@
1
+ ---
2
+ /**
3
+ * 内容卡片组件
4
+ * 用于展示带标题的 Markdown 内容
5
+ *
6
+ * 使用方式:
7
+ * <ContentCard title="关于这个博客">
8
+ * 这里是 **Markdown** 内容...
9
+ * </ContentCard>
10
+ */
11
+
12
+ export interface Props {
13
+ title: string;
14
+ class?: string;
15
+ }
16
+
17
+ const { title, class: className = '' } = Astro.props;
18
+ ---
19
+
20
+ <div class={`card ${className}`}>
21
+ <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">
22
+ {title}
23
+ </h2>
24
+ <div class="prose-custom">
25
+ <slot />
26
+ </div>
27
+ </div>
@@ -0,0 +1,77 @@
1
+ ---
2
+ /**
3
+ * 图标卡片组件
4
+ * 用于展示兴趣爱好、特点等带图标的列表
5
+ *
6
+ * 使用方式:
7
+ * <IconCard title="兴趣爱好">
8
+ * <IconCard.Item icon="code" color="primary" title="编程" description="热爱探索新技术" />
9
+ * </IconCard>
10
+ *
11
+ * 或者简单用法:
12
+ * <IconCard title="兴趣爱好" items={[{icon: 'code', title: '编程', description: '...'}]} />
13
+ */
14
+
15
+ export interface IconItem {
16
+ icon: 'code' | 'book' | 'music' | 'game' | 'travel' | 'photo' | 'movie' | 'sport' | 'video';
17
+ title: string;
18
+ description: string;
19
+ color?: 'primary' | 'secondary' | 'accent';
20
+ }
21
+
22
+ export interface Props {
23
+ title: string;
24
+ items?: IconItem[];
25
+ }
26
+
27
+ const { title, items } = Astro.props;
28
+
29
+ const bgColorMap = {
30
+ primary: 'bg-primary-100 dark:bg-primary-900/30',
31
+ secondary: 'bg-secondary-100 dark:bg-secondary-900/30',
32
+ accent: 'bg-accent-100 dark:bg-accent-900/30'
33
+ };
34
+
35
+ const iconColorMap = {
36
+ primary: 'text-primary-500',
37
+ secondary: 'text-secondary-500',
38
+ accent: 'text-accent-500'
39
+ };
40
+
41
+ const icons = {
42
+ code: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
43
+ book: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253',
44
+ music: 'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3',
45
+ game: 'M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664zM21 12a9 9 0 11-18 0 9 9 0 0118 0z',
46
+ travel: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
47
+ photo: 'M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9zM15 13a3 3 0 11-6 0 3 3 0 016 0z',
48
+ movie: 'M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z',
49
+ sport: 'M13 10V3L4 14h7v7l9-11h-7z',
50
+ video: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z'
51
+ };
52
+ ---
53
+
54
+ <div class="card mb-6 last:mb-0 h-full">
55
+ <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">
56
+ {title}
57
+ </h2>
58
+ <div class="space-y-4">
59
+ {items ? (
60
+ items.map((item) => (
61
+ <div class="flex items-start space-x-3">
62
+ <div class={`w-8 h-8 ${bgColorMap[item.color || 'primary']} rounded-lg flex items-center justify-center flex-shrink-0 mt-1`}>
63
+ <svg class={`w-4 h-4 ${iconColorMap[item.color || 'primary']}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
64
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={icons[item.icon]} />
65
+ </svg>
66
+ </div>
67
+ <div>
68
+ <h3 class="font-semibold text-slate-900 dark:text-slate-100">{item.title}</h3>
69
+ <p class="text-slate-600 dark:text-slate-400 text-sm">{item.description}</p>
70
+ </div>
71
+ </div>
72
+ ))
73
+ ) : (
74
+ <slot />
75
+ )}
76
+ </div>
77
+ </div>
@@ -0,0 +1,54 @@
1
+ ---
2
+ /**
3
+ * 社交链接组件
4
+ *
5
+ * 使用方式:
6
+ * <SocialLinks /> // 使用配置文件中的社交链接
7
+ *
8
+ * 或传入自定义链接:
9
+ * <SocialLinks links={[
10
+ * { type: 'github', url: 'https://github.com/username' },
11
+ * { type: 'twitter', url: 'https://twitter.com/username', icon: '...' }
12
+ * ]} />
13
+ */
14
+
15
+ import {
16
+ socialLinks as configSocialLinks,
17
+ defaultIcons,
18
+ type SocialLink
19
+ } from '../../config';
20
+
21
+ export interface Props {
22
+ links?: SocialLink[];
23
+ }
24
+
25
+ const { links } = Astro.props;
26
+
27
+ // 如果没有传入 links,则使用配置文件中的社交链接
28
+ const socialLinksData: SocialLink[] = links || configSocialLinks;
29
+
30
+ /**
31
+ * 获取图标路径,优先使用自定义icon,否则使用默认图标
32
+ */
33
+ function getIcon(link: SocialLink): string {
34
+ return link.icon || defaultIcons[link.type] || '';
35
+ }
36
+ ---
37
+
38
+ {socialLinksData.length > 0 && (
39
+ <div class="flex justify-center space-x-6 mt-8">
40
+ {socialLinksData.map((link) => (
41
+ <a
42
+ href={link.url}
43
+ target={link.type === 'email' ? undefined : '_blank'}
44
+ rel={link.type === 'email' ? undefined : 'noopener noreferrer'}
45
+ class="text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
46
+ aria-label={link.label || link.type}
47
+ >
48
+ <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
49
+ <path d={getIcon(link)} />
50
+ </svg>
51
+ </a>
52
+ ))}
53
+ </div>
54
+ )}
@@ -0,0 +1,65 @@
1
+ ---
2
+ /**
3
+ * 标签卡片组件
4
+ * 用于展示技术栈、技能等带标签的内容
5
+ *
6
+ * 使用方式:
7
+ * <TagCard title="技术栈">
8
+ * <TagCard.Group name="前端" color="primary" tags={['Vue', 'React']} />
9
+ * <TagCard.Group name="后端" color="secondary" tags={['Node.js']} />
10
+ * </TagCard>
11
+ *
12
+ * 或者简单用法:
13
+ * <TagCard title="技术栈" groups={[{name: '前端', tags: ['Vue'], color: 'primary'}]} />
14
+ */
15
+
16
+ export interface TagGroup {
17
+ name: string;
18
+ tags: string[];
19
+ color?: 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
20
+ }
21
+
22
+ export interface Props {
23
+ title: string;
24
+ groups?: TagGroup[];
25
+ }
26
+
27
+ const { title, groups } = Astro.props;
28
+
29
+ const colorStyles = {
30
+ primary: { bg: '#dbeafe', text: '#1d4ed8' },
31
+ secondary: { bg: '#e0e7ff', text: '#4338ca' },
32
+ accent: { bg: '#ede9fe', text: '#6d28d9' },
33
+ success: { bg: '#dcfce7', text: '#15803d' },
34
+ warning: { bg: '#fef9c3', text: '#a16207' },
35
+ danger: { bg: '#fee2e2', text: '#b91c1c' },
36
+ info: { bg: '#cffafe', text: '#0e7490' }
37
+ };
38
+ ---
39
+
40
+ <div class="card mb-6 last:mb-0 h-full">
41
+ <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">
42
+ {title}
43
+ </h2>
44
+ <div class="space-y-1">
45
+ {groups ? (
46
+ groups.map((group) => (
47
+ <div>
48
+ <h3 class="font-semibold text-slate-900 dark:text-slate-100 mb-2">{group.name}</h3>
49
+ <div class="flex flex-wrap gap-2">
50
+ {group.tags.map((tag) => (
51
+ <span
52
+ class="px-3 py-1 rounded-full text-sm"
53
+ style={`background-color: ${colorStyles[group.color || 'primary'].bg}; color: ${colorStyles[group.color || 'primary'].text};`}
54
+ >
55
+ {tag}
56
+ </span>
57
+ ))}
58
+ </div>
59
+ </div>
60
+ ))
61
+ ) : (
62
+ <slot />
63
+ )}
64
+ </div>
65
+ </div>
@@ -0,0 +1,33 @@
1
+ ---
2
+ /**
3
+ * 标签组组件 - 配合 TagCard 使用
4
+ *
5
+ * 使用方式:
6
+ * <TagGroup name="前端技术" color="primary" tags={['Vue', 'React', 'TypeScript']} />
7
+ */
8
+
9
+ export interface Props {
10
+ name: string;
11
+ tags: string[];
12
+ color?: 'primary' | 'secondary' | 'accent';
13
+ }
14
+
15
+ const { name, tags, color = 'primary' } = Astro.props;
16
+
17
+ const colorMap = {
18
+ primary: 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300',
19
+ secondary: 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-300',
20
+ accent: 'bg-accent-100 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300'
21
+ };
22
+ ---
23
+
24
+ <div>
25
+ <h3 class="font-semibold text-slate-900 dark:text-slate-100 mb-2">{name}</h3>
26
+ <div class="flex flex-wrap gap-2">
27
+ {tags.map((tag) => (
28
+ <span class={`px-3 py-1 rounded-full text-sm ${colorMap[color]}`}>
29
+ {tag}
30
+ </span>
31
+ ))}
32
+ </div>
33
+ </div>
@@ -0,0 +1,52 @@
1
+ ---
2
+ /**
3
+ * 时间线卡片组件
4
+ * 用于展示发展历程、里程碑等
5
+ *
6
+ * 使用方式:
7
+ * <TimelineCard title="发展历程" events={[
8
+ * { year: '2025', title: '博客创建', description: '...', color: 'primary' }
9
+ * ]} />
10
+ */
11
+
12
+ export interface TimelineEvent {
13
+ year: string;
14
+ title: string;
15
+ description: string;
16
+ color?: 'primary' | 'secondary' | 'accent';
17
+ }
18
+
19
+ export interface Props {
20
+ title: string;
21
+ events: TimelineEvent[];
22
+ }
23
+
24
+ const { title, events } = Astro.props;
25
+
26
+ const bgColorMap = {
27
+ primary: 'bg-primary-500',
28
+ secondary: 'bg-secondary-500',
29
+ accent: 'bg-accent-500'
30
+ };
31
+ ---
32
+
33
+ <div class="card">
34
+ <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">
35
+ {title}
36
+ </h2>
37
+ <div class="space-y-8">
38
+ {events.map((event) => (
39
+ <div class="flex items-start space-x-4">
40
+ <div class={`flex-shrink-0 w-12 h-12 ${bgColorMap[event.color || 'primary']} rounded-full flex items-center justify-center text-white font-bold`}>
41
+ {event.year}
42
+ </div>
43
+ <div>
44
+ <h3 class="font-semibold text-slate-900 dark:text-slate-100">{event.title}</h3>
45
+ <p class="text-slate-600 dark:text-slate-400 text-sm">
46
+ {event.description}
47
+ </p>
48
+ </div>
49
+ </div>
50
+ ))}
51
+ </div>
52
+ </div>
@@ -0,0 +1,198 @@
1
+ <template>
2
+ <div class="fixed top-24 right-4 z-40">
3
+ <!-- 触发按钮 -->
4
+ <div
5
+ class="relative"
6
+ @mouseenter="showToc = true"
7
+ @mouseleave="showToc = false"
8
+ >
9
+ <button
10
+ class="p-3 bg-white dark:bg-slate-800 rounded-full shadow-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
11
+ :class="{ 'bg-primary-50 dark:bg-primary-900/30': showToc }"
12
+ aria-label="显示目录"
13
+ >
14
+ <svg class="w-5 h-5 text-slate-600 dark:text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
16
+ </svg>
17
+ </button>
18
+
19
+ <!-- 目录面板 -->
20
+ <transition
21
+ enter-active-class="transition ease-out duration-200"
22
+ enter-from-class="opacity-0 translate-x-2"
23
+ enter-to-class="opacity-100 translate-x-0"
24
+ leave-active-class="transition ease-in duration-150"
25
+ leave-from-class="opacity-100 translate-x-0"
26
+ leave-to-class="opacity-0 translate-x-2"
27
+ >
28
+ <div
29
+ v-if="showToc && headings.length > 0"
30
+ class="absolute top-0 right-14 w-72 max-h-[70vh] bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 overflow-hidden"
31
+ >
32
+ <!-- 标题 -->
33
+ <div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
34
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 flex items-center gap-2">
35
+ <svg class="w-4 h-4 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
37
+ </svg>
38
+ 页面目录
39
+ </h3>
40
+ </div>
41
+
42
+ <!-- 目录列表 -->
43
+ <nav class="p-3 overflow-y-auto max-h-[calc(70vh-50px)]">
44
+ <ul class="space-y-1">
45
+ <li
46
+ v-for="heading in headings"
47
+ :key="heading.id"
48
+ :class="getIndentClass(heading.level)"
49
+ >
50
+ <a
51
+ :href="`#${heading.id}`"
52
+ @click.prevent="scrollToHeading(heading.id)"
53
+ class="block px-3 py-2 text-sm rounded-lg transition-colors"
54
+ :class="[
55
+ activeId === heading.id
56
+ ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 font-medium'
57
+ : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-slate-200'
58
+ ]"
59
+ >
60
+ <span class="line-clamp-2">{{ heading.text }}</span>
61
+ </a>
62
+ </li>
63
+ </ul>
64
+ </nav>
65
+
66
+ <!-- 进度条 -->
67
+ <div class="px-4 py-2 border-t border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
68
+ <div class="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
69
+ <span>阅读进度</span>
70
+ <span>{{ Math.round(progress * 100) }}%</span>
71
+ </div>
72
+ <div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5">
73
+ <div
74
+ class="bg-primary-500 h-1.5 rounded-full transition-all duration-300"
75
+ :style="{ width: `${progress * 100}%` }"
76
+ ></div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </transition>
81
+ </div>
82
+ </div>
83
+ </template>
84
+
85
+ <script setup lang="ts">
86
+ import { ref, onMounted, onUnmounted } from 'vue'
87
+
88
+ interface Heading {
89
+ id: string
90
+ text: string
91
+ level: number
92
+ }
93
+
94
+ const showToc = ref(false)
95
+ const headings = ref<Heading[]>([])
96
+ const activeId = ref('')
97
+ const progress = ref(0)
98
+
99
+ // 获取缩进样式
100
+ function getIndentClass(level: number): string {
101
+ switch (level) {
102
+ case 2: return 'pl-0'
103
+ case 3: return 'pl-3'
104
+ case 4: return 'pl-6'
105
+ default: return 'pl-0'
106
+ }
107
+ }
108
+
109
+ // 滚动到指定标题
110
+ function scrollToHeading(id: string) {
111
+ const element = document.getElementById(id)
112
+ if (element) {
113
+ const offsetTop = element.offsetTop - 100
114
+ window.scrollTo({
115
+ top: offsetTop,
116
+ behavior: 'smooth'
117
+ })
118
+ }
119
+ }
120
+
121
+ // 提取页面标题(只提取文章内容区域中有 id 的标题)
122
+ function extractHeadings() {
123
+ // 只选择 article 内有 id 属性的标题
124
+ const articleHeadings = document.querySelectorAll('article h2[id], article h3[id], article h4[id]')
125
+ headings.value = Array.from(articleHeadings)
126
+ .filter((heading) => heading.id) // 确保有 id
127
+ .map((heading) => ({
128
+ id: heading.id,
129
+ text: heading.textContent || '',
130
+ level: parseInt(heading.tagName.charAt(1))
131
+ }))
132
+ }
133
+
134
+ // 更新当前激活的标题
135
+ function updateActiveHeading() {
136
+ const articleHeadings = document.querySelectorAll('article h2[id], article h3[id], article h4[id]')
137
+ let current = ''
138
+
139
+ articleHeadings.forEach((heading) => {
140
+ if (!heading.id) return
141
+ const rect = heading.getBoundingClientRect()
142
+ if (rect.top <= 120) {
143
+ current = heading.id
144
+ }
145
+ })
146
+
147
+ activeId.value = current
148
+ }
149
+
150
+ // 更新阅读进度
151
+ function updateProgress() {
152
+ const article = document.querySelector('article')
153
+ if (!article) return
154
+
155
+ const articleTop = article.offsetTop
156
+ const articleHeight = article.offsetHeight
157
+ const windowTop = window.pageYOffset
158
+ const windowHeight = window.innerHeight
159
+
160
+ progress.value = Math.min(
161
+ Math.max((windowTop - articleTop + windowHeight / 2) / articleHeight, 0),
162
+ 1
163
+ )
164
+ }
165
+
166
+ // 滚动处理
167
+ let ticking = false
168
+ function handleScroll() {
169
+ if (!ticking) {
170
+ requestAnimationFrame(() => {
171
+ updateActiveHeading()
172
+ updateProgress()
173
+ ticking = false
174
+ })
175
+ ticking = true
176
+ }
177
+ }
178
+
179
+ onMounted(() => {
180
+ extractHeadings()
181
+ updateActiveHeading()
182
+ updateProgress()
183
+ window.addEventListener('scroll', handleScroll)
184
+ })
185
+
186
+ onUnmounted(() => {
187
+ window.removeEventListener('scroll', handleScroll)
188
+ })
189
+ </script>
190
+
191
+ <style scoped>
192
+ .line-clamp-2 {
193
+ display: -webkit-box;
194
+ -webkit-line-clamp: 2;
195
+ -webkit-box-orient: vertical;
196
+ overflow: hidden;
197
+ }
198
+ </style>