@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,90 @@
1
+ <template>
2
+ <button
3
+ @click="toggleTheme"
4
+ class="p-2 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 transition-colors"
5
+ :aria-label="isDark ? '切换到浅色模式' : '切换到深色模式'"
6
+ >
7
+ <transition name="fade" mode="out-in">
8
+ <svg
9
+ v-if="isDark"
10
+ key="sun"
11
+ class="w-5 h-5 text-slate-700 dark:text-slate-300"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ viewBox="0 0 24 24"
15
+ >
16
+ <path
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ stroke-width="2"
20
+ d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
21
+ />
22
+ </svg>
23
+ <svg
24
+ v-else
25
+ key="moon"
26
+ class="w-5 h-5 text-slate-700 dark:text-slate-300"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ viewBox="0 0 24 24"
30
+ >
31
+ <path
32
+ stroke-linecap="round"
33
+ stroke-linejoin="round"
34
+ stroke-width="2"
35
+ d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
36
+ />
37
+ </svg>
38
+ </transition>
39
+ </button>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import { ref, onMounted } from 'vue'
44
+
45
+ const isDark = ref(false)
46
+
47
+ const toggleTheme = () => {
48
+ isDark.value = !isDark.value
49
+
50
+ if (isDark.value) {
51
+ document.documentElement.classList.add('dark')
52
+ localStorage.setItem('theme', 'dark')
53
+ } else {
54
+ document.documentElement.classList.remove('dark')
55
+ localStorage.setItem('theme', 'light')
56
+ }
57
+ }
58
+
59
+ onMounted(() => {
60
+ // 检查当前主题
61
+ const savedTheme = localStorage.getItem('theme')
62
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
63
+
64
+ isDark.value = savedTheme === 'dark' || (!savedTheme && systemPrefersDark)
65
+
66
+ // 监听系统主题变化
67
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
68
+ if (!localStorage.getItem('theme')) {
69
+ isDark.value = e.matches
70
+ if (e.matches) {
71
+ document.documentElement.classList.add('dark')
72
+ } else {
73
+ document.documentElement.classList.remove('dark')
74
+ }
75
+ }
76
+ })
77
+ })
78
+ </script>
79
+
80
+ <style scoped>
81
+ .fade-enter-active,
82
+ .fade-leave-active {
83
+ transition: opacity 0.2s ease;
84
+ }
85
+
86
+ .fade-enter-from,
87
+ .fade-leave-to {
88
+ opacity: 0;
89
+ }
90
+ </style>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import PageLayout from './PageLayout.astro';
3
+
4
+ export interface Props {
5
+ title?: string;
6
+ description?: string;
7
+ }
8
+
9
+ const { title, description } = Astro.props;
10
+ ---
11
+
12
+ <PageLayout
13
+ title={title}
14
+ description={description}
15
+ showSidebar={false}
16
+ >
17
+ <slot />
18
+ </PageLayout>
@@ -0,0 +1,362 @@
1
+ ---
2
+ import type { SEOProps } from '../types';
3
+ import { siteConfig, defaultSEO } from '../config/site';
4
+ import '../styles/global.css';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ export interface Props {
9
+ title?: string;
10
+ description?: string;
11
+ image?: string;
12
+ type?: 'website' | 'article';
13
+ publishedTime?: string;
14
+ modifiedTime?: string;
15
+ tags?: string[];
16
+ }
17
+
18
+ const {
19
+ title = defaultSEO.title,
20
+ description = defaultSEO.description,
21
+ image = defaultSEO.image,
22
+ type = 'website',
23
+ publishedTime,
24
+ modifiedTime,
25
+ tags
26
+ } = Astro.props;
27
+
28
+ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
29
+ const fullTitle = title === siteConfig.title ? title : `${title} | ${siteConfig.title}`;
30
+ const fullImage = new URL(image, Astro.site);
31
+
32
+ // 检查 favicon 文件是否存在,不存在则使用 avatar
33
+ const publicDir = path.join(process.cwd(), 'public');
34
+ const faviconSvgExists = fs.existsSync(path.join(publicDir, 'favicon.svg'));
35
+ const faviconIcoExists = fs.existsSync(path.join(publicDir, 'favicon.ico'));
36
+ const faviconHref = faviconSvgExists ? '/favicon.svg' : faviconIcoExists ? '/favicon.ico' : siteConfig.avatar;
37
+ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'image/x-icon' : 'image/jpeg';
38
+ ---
39
+
40
+ <!DOCTYPE html>
41
+ <html lang="zh-CN" class="scroll-smooth">
42
+ <head>
43
+ <meta charset="UTF-8" />
44
+ <meta name="description" content={description} />
45
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
46
+ <link rel="icon" type={faviconType} href={faviconHref} />
47
+ <meta name="generator" content={Astro.generator} />
48
+
49
+ <!-- SEO -->
50
+ <title>{fullTitle}</title>
51
+ <link rel="canonical" href={canonicalURL} />
52
+
53
+ <!-- Open Graph -->
54
+ <meta property="og:type" content={type} />
55
+ <meta property="og:title" content={fullTitle} />
56
+ <meta property="og:description" content={description} />
57
+ <meta property="og:url" content={canonicalURL} />
58
+ <meta property="og:image" content={fullImage} />
59
+ <meta property="og:site_name" content={siteConfig.title} />
60
+
61
+ {publishedTime && (
62
+ <meta property="article:published_time" content={publishedTime} />
63
+ )}
64
+ {modifiedTime && (
65
+ <meta property="article:modified_time" content={modifiedTime} />
66
+ )}
67
+ {tags && tags.map(tag => (
68
+ <meta property="article:tag" content={tag} />
69
+ ))}
70
+
71
+ <!-- Twitter -->
72
+ <meta name="twitter:card" content="summary_large_image" />
73
+ <meta name="twitter:title" content={fullTitle} />
74
+ <meta name="twitter:description" content={description} />
75
+ <meta name="twitter:image" content={fullImage} />
76
+
77
+ <!-- RSS -->
78
+ <link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/rss.xml" />
79
+
80
+ <!-- 预加载关键资源 -->
81
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
82
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
83
+
84
+ <!-- 图标库 -->
85
+ <!-- Font Awesome -->
86
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" />
87
+ <!-- Material Icons -->
88
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round" />
89
+ <!-- Bootstrap Icons -->
90
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
91
+ <!-- Remix Icon -->
92
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" />
93
+ <!-- KaTeX for LaTeX math rendering -->
94
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous" />
95
+ <!-- Ionicons -->
96
+ <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
97
+
98
+ <!-- 主题检测脚本 -->
99
+ <script is:inline>
100
+ // 在页面加载前检测主题,避免闪烁
101
+ // 默认使用深色模式
102
+ const theme = (() => {
103
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
104
+ return localStorage.getItem('theme');
105
+ }
106
+ // 默认深色模式,除非用户系统偏好浅色
107
+ if (window.matchMedia('(prefers-color-scheme: light)').matches) {
108
+ return 'light';
109
+ }
110
+ return 'dark';
111
+ })();
112
+
113
+ if (theme === 'light') {
114
+ document.documentElement.classList.remove('dark');
115
+ } else {
116
+ document.documentElement.classList.add('dark');
117
+ }
118
+ window.__theme = theme;
119
+ </script>
120
+ </head>
121
+ <body class="min-h-screen">
122
+ <div id="app" class="flex flex-col min-h-screen">
123
+ <slot />
124
+ </div>
125
+
126
+ <!-- Mermaid 容器渲染 -->
127
+ <script src="/js/mermaid-container.js" is:inline></script>
128
+
129
+ <!-- Tabs 组件初始化 -->
130
+ <script src="/js/tabs-init.js" is:inline></script>
131
+
132
+ <!-- 全局脚本 -->
133
+ <script>
134
+ // 回到顶部功能
135
+ const backToTop = document.querySelector('.back-to-top');
136
+ if (backToTop) {
137
+ window.addEventListener('scroll', () => {
138
+ if (window.scrollY > 300) {
139
+ backToTop.classList.add('show');
140
+ } else {
141
+ backToTop.classList.remove('show');
142
+ }
143
+ });
144
+
145
+ backToTop.addEventListener('click', () => {
146
+ window.scrollTo({ top: 0, behavior: 'smooth' });
147
+ });
148
+ }
149
+
150
+ // 平滑滚动锚点链接
151
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
152
+ anchor.addEventListener('click', function (this: HTMLAnchorElement, e: Event) {
153
+ e.preventDefault();
154
+ const href = this.getAttribute('href');
155
+ if (href) {
156
+ const target = document.querySelector(href);
157
+ if (target) {
158
+ target.scrollIntoView({ behavior: 'smooth' });
159
+ }
160
+ }
161
+ });
162
+ });
163
+
164
+ // 代码块增强功能
165
+ function enhanceCodeBlocks() {
166
+ const codeBlocks = document.querySelectorAll('pre:not([data-enhanced])');
167
+ const COLLAPSE_THRESHOLD = 15; // 超过15行才显示收缩功能
168
+
169
+ codeBlocks.forEach((pre) => {
170
+ // 标记为已处理
171
+ pre.setAttribute('data-enhanced', 'true');
172
+
173
+ // 获取语言信息
174
+ const code = pre.querySelector('code');
175
+ let lang = 'code';
176
+
177
+ // 跳过 mermaid 代码块
178
+ if (code?.classList.contains('language-mermaid') ||
179
+ pre.classList.contains('mermaid') ||
180
+ pre.closest('.mermaid-container')) {
181
+ return;
182
+ }
183
+
184
+ if (code) {
185
+ // 从 class 中获取语言
186
+ const classList = code.className.split(' ');
187
+ for (const cls of classList) {
188
+ if (cls.startsWith('language-')) {
189
+ lang = cls.replace('language-', '');
190
+ break;
191
+ }
192
+ }
193
+ }
194
+
195
+ // 从 data-language 属性获取(shiki 用这个)
196
+ const preEl = pre as HTMLPreElement;
197
+ if (preEl.dataset.language) {
198
+ lang = preEl.dataset.language;
199
+ }
200
+
201
+ // 计算代码行数
202
+ const codeText = code ? code.textContent : pre.textContent;
203
+ const lineCount = (codeText || '').split('\n').length;
204
+ const shouldCollapse = lineCount > COLLAPSE_THRESHOLD;
205
+
206
+ // 创建包装器
207
+ const wrapper = document.createElement('div');
208
+ wrapper.className = 'code-block-wrapper' + (shouldCollapse ? ' collapsed' : '');
209
+
210
+ // 创建头部
211
+ const header = document.createElement('div');
212
+ header.className = 'code-block-header';
213
+ header.innerHTML = `
214
+ <span class="code-block-lang">${lang}</span>
215
+ <div class="code-block-actions">
216
+ ${shouldCollapse ? `
217
+ <button class="code-block-btn collapse-btn" title="展开/收缩">
218
+ <svg class="collapse-icon transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
219
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
220
+ </svg>
221
+ <span class="collapse-text">展开</span>
222
+ </button>
223
+ ` : ''}
224
+ <button class="code-block-btn copy-btn" title="复制代码">
225
+ <svg class="copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
226
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
227
+ </svg>
228
+ <span class="copy-text">复制</span>
229
+ </button>
230
+ </div>
231
+ `;
232
+
233
+ // 创建内容区
234
+ const content = document.createElement('div');
235
+ content.className = 'code-block-content';
236
+
237
+ // 插入DOM
238
+ if (!pre.parentNode) return;
239
+ pre.parentNode.insertBefore(wrapper, pre);
240
+ wrapper.appendChild(header);
241
+ wrapper.appendChild(content);
242
+ content.appendChild(pre);
243
+
244
+ // 如果需要收缩,添加展开按钮和底部收起按钮
245
+ if (shouldCollapse) {
246
+ // 展开按钮(收缩状态显示)
247
+ const expandOverlay = document.createElement('div');
248
+ expandOverlay.className = 'code-block-expand';
249
+ expandOverlay.innerHTML = `
250
+ <button class="expand-btn">
251
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
252
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
253
+ </svg>
254
+ <span>展开代码 (${lineCount} 行)</span>
255
+ </button>
256
+ `;
257
+ content.appendChild(expandOverlay);
258
+
259
+ // 底部收起按钮(展开状态显示)
260
+ const collapseOverlay = document.createElement('div');
261
+ collapseOverlay.className = 'code-block-collapse';
262
+ collapseOverlay.innerHTML = `
263
+ <button class="collapse-bottom-btn">
264
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
265
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
266
+ </svg>
267
+ <span>收起代码</span>
268
+ </button>
269
+ `;
270
+ content.appendChild(collapseOverlay);
271
+
272
+ // 展开按钮点击事件
273
+ const expandBtn = expandOverlay.querySelector('.expand-btn');
274
+ if (expandBtn) {
275
+ expandBtn.addEventListener('click', () => {
276
+ wrapper.classList.remove('collapsed');
277
+ const collapseText = header.querySelector('.collapse-text') as HTMLElement | null;
278
+ if (collapseText) collapseText.textContent = '收缩';
279
+ });
280
+ }
281
+
282
+ // 底部收起按钮点击事件
283
+ const collapseBottomBtn = collapseOverlay.querySelector('.collapse-bottom-btn');
284
+ if (collapseBottomBtn) {
285
+ collapseBottomBtn.addEventListener('click', () => {
286
+ wrapper.classList.add('collapsed');
287
+ const collapseText = header.querySelector('.collapse-text') as HTMLElement | null;
288
+ if (collapseText) collapseText.textContent = '展开';
289
+ // 滚动到代码块顶部
290
+ wrapper.scrollIntoView({ behavior: 'smooth', block: 'start' });
291
+ });
292
+ }
293
+ }
294
+
295
+ // 复制按钮点击事件
296
+ const copyBtn = header.querySelector('.copy-btn') as HTMLElement | null;
297
+ if (copyBtn) {
298
+ copyBtn.addEventListener('click', async () => {
299
+ const textToCopy = code ? code.textContent : pre.textContent;
300
+ try {
301
+ await navigator.clipboard.writeText(textToCopy || '');
302
+ copyBtn.classList.add('copied');
303
+ const copyText = copyBtn.querySelector('.copy-text') as HTMLElement | null;
304
+ const copyIcon = copyBtn.querySelector('.copy-icon') as HTMLElement | null;
305
+ if (copyText) copyText.textContent = '已复制';
306
+ if (copyIcon) copyIcon.innerHTML = `
307
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
308
+ `;
309
+
310
+ setTimeout(() => {
311
+ copyBtn.classList.remove('copied');
312
+ if (copyText) copyText.textContent = '复制';
313
+ if (copyIcon) copyIcon.innerHTML = `
314
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
315
+ `;
316
+ }, 2000);
317
+ } catch (err) {
318
+ console.error('复制失败:', err);
319
+ }
320
+ });
321
+ }
322
+
323
+ // 收缩按钮点击事件
324
+ const collapseBtn = header.querySelector('.collapse-btn') as HTMLElement | null;
325
+ if (collapseBtn) {
326
+ collapseBtn.addEventListener('click', () => {
327
+ const isCollapsed = wrapper.classList.toggle('collapsed');
328
+ const collapseText = collapseBtn.querySelector('.collapse-text') as HTMLElement | null;
329
+ if (collapseText) collapseText.textContent = isCollapsed ? '展开' : '收缩';
330
+ });
331
+ }
332
+ });
333
+ }
334
+
335
+ // 页面加载后执行
336
+ enhanceCodeBlocks();
337
+
338
+ // 监听DOM变化(处理动态加载的内容)
339
+ const observer = new MutationObserver((mutations) => {
340
+ let hasNewCodeBlocks = false;
341
+ mutations.forEach((mutation) => {
342
+ if (mutation.addedNodes.length) {
343
+ mutation.addedNodes.forEach((node) => {
344
+ if (node.nodeType === 1) {
345
+ const el = node as Element;
346
+ if (el.tagName === 'PRE' || el.querySelector?.('pre:not([data-enhanced])')) {
347
+ hasNewCodeBlocks = true;
348
+ }
349
+ }
350
+ });
351
+ }
352
+ });
353
+ if (hasNewCodeBlocks) {
354
+ enhanceCodeBlocks();
355
+ }
356
+ });
357
+
358
+ observer.observe(document.body, { childList: true, subtree: true });
359
+ </script>
360
+
361
+ </body>
362
+ </html>
@@ -0,0 +1,217 @@
1
+ ---
2
+ import BaseLayout from './BaseLayout.astro';
3
+ import Header from '../components/layout/Header.astro';
4
+ import Footer from '../components/layout/Footer.astro';
5
+ import Sidebar from '../components/layout/Sidebar.astro';
6
+ import SidebarToggle from '../components/ui/SidebarToggle.vue';
7
+
8
+ export interface Props {
9
+ title?: string;
10
+ description?: string;
11
+ image?: string;
12
+ type?: 'website' | 'article';
13
+ publishedTime?: string;
14
+ modifiedTime?: string;
15
+ tags?: string[];
16
+ showSidebar?: boolean;
17
+ showToc?: boolean;
18
+ }
19
+
20
+ const {
21
+ title,
22
+ description,
23
+ image,
24
+ type,
25
+ publishedTime,
26
+ modifiedTime,
27
+ tags,
28
+ showSidebar = true,
29
+ showToc = false
30
+ } = Astro.props;
31
+ ---
32
+
33
+ <BaseLayout
34
+ title={title}
35
+ description={description}
36
+ image={image}
37
+ type={type}
38
+ publishedTime={publishedTime}
39
+ modifiedTime={modifiedTime}
40
+ tags={tags}
41
+ >
42
+ <Header />
43
+
44
+ <main class="flex-1 flex">
45
+ <!-- 左侧边栏 - 桌面端 -->
46
+ {showSidebar && (
47
+ <aside class="hidden lg:block shrink-0 transition-all duration-300 relative" data-sidebar style="width: var(--sidebar-width, 256px);">
48
+ <div class="sticky top-20 h-[calc(100vh-5rem)] overflow-y-auto">
49
+ <Sidebar />
50
+ </div>
51
+ <!-- 拖拽调整宽度的手柄 -->
52
+ <div
53
+ class="absolute top-0 right-0 w-1 h-full cursor-col-resize group z-10"
54
+ data-sidebar-resizer
55
+ >
56
+ <div class="absolute top-0 right-0 w-4 h-full -mr-2 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
57
+ <div class="w-1 h-16 bg-primary-400 dark:bg-primary-500 rounded-full"></div>
58
+ </div>
59
+ </div>
60
+ </aside>
61
+ )}
62
+
63
+ <!-- 移动端侧边栏遮罩 -->
64
+ {showSidebar && (
65
+ <div
66
+ class="lg:hidden fixed inset-0 bg-black/50 z-40 hidden"
67
+ data-sidebar-overlay
68
+ onclick="this.classList.add('hidden'); document.querySelector('[data-mobile-sidebar]').classList.add('-translate-x-full');"
69
+ ></div>
70
+ )}
71
+
72
+ <!-- 移动端侧边栏 -->
73
+ {showSidebar && (
74
+ <aside
75
+ class="lg:hidden fixed top-16 left-0 w-72 h-[calc(100vh-4rem)] bg-white dark:bg-slate-900 z-50 transform -translate-x-full transition-transform duration-300 shadow-xl overflow-y-auto"
76
+ data-mobile-sidebar
77
+ >
78
+ <div class="p-4">
79
+ <Sidebar />
80
+ </div>
81
+ </aside>
82
+ )}
83
+
84
+ <!-- 主内容区 -->
85
+ <div class={`flex-1 transition-all duration-300 ${showSidebar ? 'lg:px-8' : ''} ${showToc ? 'xl:pr-64' : ''} relative`} data-main-content>
86
+ <!-- 侧边栏切换按钮 - 桌面端固定位置 -->
87
+ {showSidebar && (
88
+ <div class="hidden lg:block fixed top-20 left-1 z-40 transition-all duration-300" data-sidebar-toggle>
89
+ <SidebarToggle client:load />
90
+ </div>
91
+ )}
92
+
93
+ <!-- 侧边栏切换按钮 - 移动端浮动按钮 -->
94
+ {showSidebar && (
95
+ <button
96
+ class="lg:hidden fixed bottom-20 left-4 z-40 p-3 bg-primary-500 hover:bg-primary-600 text-white rounded-full shadow-lg transition-colors"
97
+ data-mobile-sidebar-toggle
98
+ aria-label="打开侧边栏"
99
+ onclick="document.querySelector('[data-sidebar-overlay]').classList.remove('hidden'); document.querySelector('[data-mobile-sidebar]').classList.remove('-translate-x-full');"
100
+ >
101
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
103
+ </svg>
104
+ </button>
105
+ )}
106
+
107
+ <div class="container mx-auto px-4 py-8 max-w-4xl">
108
+ <slot />
109
+ </div>
110
+ </div>
111
+
112
+ <!-- 右侧TOC -->
113
+ {showToc && (
114
+ <aside class="hidden xl:block w-64 shrink-0">
115
+ <div class="sticky top-20 h-[calc(100vh-5rem)] overflow-y-auto">
116
+ <slot name="toc" />
117
+ </div>
118
+ </aside>
119
+ )}
120
+ </main>
121
+
122
+ <Footer />
123
+
124
+ <!-- 回到顶部按钮 -->
125
+ <button class="back-to-top" aria-label="回到顶部">
126
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
128
+ </svg>
129
+ </button>
130
+ </BaseLayout>
131
+
132
+ <script>
133
+ // 侧边栏拖拽调整宽度
134
+ function initSidebarResizer() {
135
+ const sidebar = document.querySelector('[data-sidebar]') as HTMLElement;
136
+ const resizer = document.querySelector('[data-sidebar-resizer]') as HTMLElement;
137
+
138
+ if (!sidebar || !resizer) return;
139
+
140
+ // 从 localStorage 恢复宽度
141
+ const savedWidth = localStorage.getItem('sidebar-width');
142
+ if (savedWidth) {
143
+ sidebar.style.setProperty('--sidebar-width', savedWidth);
144
+ }
145
+
146
+ let isResizing = false;
147
+ let startX = 0;
148
+ let startWidth = 0;
149
+
150
+ const startResize = (e: MouseEvent) => {
151
+ isResizing = true;
152
+ startX = e.clientX;
153
+ startWidth = sidebar.offsetWidth;
154
+
155
+ // 禁用过渡效果以获得流畅拖拽
156
+ sidebar.style.transition = 'none';
157
+
158
+ // 添加全局样式防止选中文本
159
+ document.body.style.cursor = 'col-resize';
160
+ document.body.style.userSelect = 'none';
161
+
162
+ document.addEventListener('mousemove', doResize);
163
+ document.addEventListener('mouseup', stopResize);
164
+ };
165
+
166
+ const doResize = (e: MouseEvent) => {
167
+ if (!isResizing) return;
168
+
169
+ const diff = e.clientX - startX;
170
+ const newWidth = Math.min(Math.max(startWidth + diff, 200), 500); // 最小200px,最大500px
171
+
172
+ sidebar.style.setProperty('--sidebar-width', `${newWidth}px`);
173
+ };
174
+
175
+ const stopResize = () => {
176
+ if (!isResizing) return;
177
+
178
+ isResizing = false;
179
+
180
+ // 恢复过渡效果
181
+ sidebar.style.transition = '';
182
+
183
+ // 恢复全局样式
184
+ document.body.style.cursor = '';
185
+ document.body.style.userSelect = '';
186
+
187
+ // 保存宽度到 localStorage
188
+ const currentWidth = getComputedStyle(sidebar).getPropertyValue('--sidebar-width');
189
+ if (currentWidth) {
190
+ localStorage.setItem('sidebar-width', currentWidth.trim());
191
+ }
192
+
193
+ document.removeEventListener('mousemove', doResize);
194
+ document.removeEventListener('mouseup', stopResize);
195
+ };
196
+
197
+ resizer.addEventListener('mousedown', startResize);
198
+ }
199
+
200
+ // DOM 加载完成后初始化
201
+ if (document.readyState === 'loading') {
202
+ document.addEventListener('DOMContentLoaded', initSidebarResizer);
203
+ } else {
204
+ initSidebarResizer();
205
+ }
206
+ </script>
207
+
208
+ <style>
209
+ /* 拖拽时的视觉反馈 */
210
+ [data-sidebar-resizer]:hover {
211
+ background: linear-gradient(to right, transparent, rgba(var(--color-primary-500), 0.1), transparent);
212
+ }
213
+
214
+ [data-sidebar-resizer]:active {
215
+ background: linear-gradient(to right, transparent, rgba(var(--color-primary-500), 0.2), transparent);
216
+ }
217
+ </style>