@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,114 @@
1
+ ---
2
+ /**
3
+ * Bilibili 视频嵌入组件
4
+ *
5
+ * 使用方法:
6
+ * <Bilibili bvid="BV1xx411c7mD" />
7
+ * <Bilibili aid="170001" />
8
+ * <Bilibili url="https://www.bilibili.com/video/BV1xx411c7mD" />
9
+ * <Bilibili bvid="BV1xx411c7mD" page={2} />
10
+ */
11
+
12
+ interface Props {
13
+ /** BV 号 */
14
+ bvid?: string;
15
+ /** AV 号 */
16
+ aid?: string | number;
17
+ /** Bilibili 视频完整 URL */
18
+ url?: string;
19
+ /** 视频标题 (用于无障碍) */
20
+ title?: string;
21
+ /** 分P (从1开始) */
22
+ page?: number;
23
+ /** 开始时间 (秒) */
24
+ start?: number;
25
+ /** 是否高清 */
26
+ highQuality?: boolean;
27
+ /** 是否自动播放 */
28
+ autoplay?: boolean;
29
+ /** 是否静音 */
30
+ muted?: boolean;
31
+ /** 自定义宽高比 (默认 16/9) */
32
+ aspectRatio?: string;
33
+ }
34
+
35
+ const {
36
+ bvid,
37
+ aid,
38
+ url,
39
+ title = 'Bilibili 视频',
40
+ page = 1,
41
+ start = 0,
42
+ highQuality = true,
43
+ autoplay = false,
44
+ muted = false,
45
+ aspectRatio = '16/9'
46
+ } = Astro.props;
47
+
48
+ // 从 URL 中提取视频信息
49
+ function extractVideoInfo(videoUrl: string): { bvid?: string; aid?: string } {
50
+ // BV 号匹配
51
+ const bvMatch = videoUrl.match(/BV[a-zA-Z0-9]+/);
52
+ if (bvMatch) return { bvid: bvMatch[0] };
53
+
54
+ // AV 号匹配
55
+ const avMatch = videoUrl.match(/av(\d+)/i);
56
+ if (avMatch) return { aid: avMatch[1] };
57
+
58
+ return {};
59
+ }
60
+
61
+ let videoBvid = bvid;
62
+ let videoAid = aid?.toString();
63
+
64
+ if (url) {
65
+ const info = extractVideoInfo(url);
66
+ videoBvid = videoBvid || info.bvid;
67
+ videoAid = videoAid || info.aid;
68
+ }
69
+
70
+ // 构建嵌入 URL
71
+ let embedUrl = 'https://player.bilibili.com/player.html?';
72
+ const params = new URLSearchParams();
73
+
74
+ if (videoBvid) {
75
+ params.set('bvid', videoBvid);
76
+ } else if (videoAid) {
77
+ params.set('aid', videoAid);
78
+ }
79
+
80
+ params.set('page', page.toString());
81
+ if (start > 0) params.set('t', start.toString());
82
+ if (highQuality) params.set('high_quality', '1');
83
+ if (autoplay) params.set('autoplay', '1');
84
+ if (muted) params.set('muted', '1');
85
+ params.set('danmaku', '0'); // 默认关闭弹幕
86
+
87
+ embedUrl += params.toString();
88
+
89
+ const hasValidId = videoBvid || videoAid;
90
+ ---
91
+
92
+ {hasValidId ? (
93
+ <div class="video-container bilibili-video" style={`aspect-ratio: ${aspectRatio};`}>
94
+ <iframe
95
+ src={embedUrl}
96
+ title={title}
97
+ frameborder="0"
98
+ allowfullscreen
99
+ loading="lazy"
100
+ scrolling="no"
101
+ sandbox="allow-scripts allow-same-origin allow-popups"
102
+ ></iframe>
103
+ <div class="video-platform-badge bilibili">
104
+ <svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
105
+ <path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373z"/>
106
+ </svg>
107
+ <span>Bilibili</span>
108
+ </div>
109
+ </div>
110
+ ) : (
111
+ <div class="video-error">
112
+ <p>无效的 Bilibili 视频链接</p>
113
+ </div>
114
+ )}
@@ -0,0 +1,313 @@
1
+ ---
2
+ /**
3
+ * 内嵌幻灯片组件
4
+ *
5
+ * 使用方法:
6
+ *
7
+ * 1. 通过 iframe 嵌入已有幻灯片:
8
+ * <Slides src="/slides/demo" />
9
+ * <Slides src="/slides/demo" height="500px" />
10
+ *
11
+ * 2. 直接内嵌 Markdown 内容:
12
+ * <Slides>
13
+ * # 第一页
14
+ *
15
+ * 内容...
16
+ *
17
+ * ---
18
+ *
19
+ * # 第二页
20
+ *
21
+ * 更多内容...
22
+ * </Slides>
23
+ */
24
+
25
+ interface Props {
26
+ /** 幻灯片 URL(使用 iframe 嵌入模式) */
27
+ src?: string;
28
+ /** 容器高度 */
29
+ height?: string;
30
+ /** 宽高比(当不指定 height 时使用) */
31
+ aspectRatio?: string;
32
+ /** Reveal.js 主题 */
33
+ theme?: string;
34
+ /** 过渡效果 */
35
+ transition?: string;
36
+ /** 显示控制箭头 */
37
+ controls?: boolean;
38
+ /** 显示进度条 */
39
+ progress?: boolean;
40
+ /** 内容居中 */
41
+ center?: boolean;
42
+ /** 显示页码 */
43
+ slideNumber?: boolean;
44
+ /** 是否嵌入模式(隐藏部分 UI) */
45
+ embedded?: boolean;
46
+ /** 标题(用于无障碍) */
47
+ title?: string;
48
+ }
49
+
50
+ const {
51
+ src,
52
+ height,
53
+ aspectRatio = '16/9',
54
+ theme = 'black',
55
+ transition = 'slide',
56
+ controls = true,
57
+ progress = true,
58
+ center = true,
59
+ slideNumber = false,
60
+ embedded = true,
61
+ title = '幻灯片演示'
62
+ } = Astro.props;
63
+
64
+ // 生成唯一 ID
65
+ const slidesId = `slides-${Math.random().toString(36).substr(2, 9)}`;
66
+
67
+ // Reveal.js 配置
68
+ const revealConfig = JSON.stringify({
69
+ hash: false,
70
+ controls,
71
+ progress,
72
+ center,
73
+ transition,
74
+ slideNumber,
75
+ embedded,
76
+ keyboard: true,
77
+ overview: true,
78
+ touch: true,
79
+ });
80
+
81
+ // 获取 slot 内容
82
+ const slotContent = await Astro.slots.render('default');
83
+ const hasInlineContent = slotContent && slotContent.trim().length > 0;
84
+
85
+ // 解析 Markdown 为幻灯片结构
86
+ function parseSlides(markdown: string): string[][] {
87
+ // 移除 HTML 注释和空白
88
+ const cleanMarkdown = markdown
89
+ .replace(/<!--[\s\S]*?-->/g, '')
90
+ .trim();
91
+
92
+ // 按水平分隔符分割
93
+ const horizontalSlides = cleanMarkdown.split(/\n---\n/);
94
+
95
+ return horizontalSlides.map((hSlide) => {
96
+ // 按垂直分隔符分割
97
+ return hSlide.split(/\n----\n/).map(s => s.trim());
98
+ });
99
+ }
100
+
101
+ const slidesData = hasInlineContent ? parseSlides(slotContent) : [];
102
+ ---
103
+
104
+ {src ? (
105
+ <!-- iframe 嵌入模式 -->
106
+ <div
107
+ class="slides-embed-container"
108
+ style={height ? `height: ${height};` : `aspect-ratio: ${aspectRatio};`}
109
+ >
110
+ <iframe
111
+ src={`${src}${src.includes('?') ? '&' : '?'}embed=true`}
112
+ title={title}
113
+ class="slides-iframe"
114
+ allowfullscreen
115
+ loading="lazy"
116
+ ></iframe>
117
+ <a
118
+ href={src}
119
+ target="_blank"
120
+ class="slides-fullscreen-btn"
121
+ title="在新窗口打开"
122
+ >
123
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
124
+ <polyline points="15 3 21 3 21 9"></polyline>
125
+ <polyline points="9 21 3 21 3 15"></polyline>
126
+ <line x1="21" y1="3" x2="14" y2="10"></line>
127
+ <line x1="3" y1="21" x2="10" y2="14"></line>
128
+ </svg>
129
+ </a>
130
+ </div>
131
+ ) : hasInlineContent ? (
132
+ <!-- 内联内容模式 -->
133
+ <div
134
+ class="slides-inline-container"
135
+ style={height ? `height: ${height};` : `aspect-ratio: ${aspectRatio};`}
136
+ >
137
+ <div class="reveal" id={slidesId}>
138
+ <div class="slides">
139
+ {slidesData.map((verticalSlides) => (
140
+ <section>
141
+ {verticalSlides.map((slideContent) => (
142
+ <section data-markdown>
143
+ <textarea data-template set:text={slideContent}></textarea>
144
+ </section>
145
+ ))}
146
+ </section>
147
+ ))}
148
+ </div>
149
+ </div>
150
+
151
+ <!-- 全屏按钮 -->
152
+ <button
153
+ class="slides-fullscreen-btn"
154
+ title="全屏"
155
+ onclick={`document.getElementById('${slidesId}').requestFullscreen()`}
156
+ >
157
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
158
+ <polyline points="15 3 21 3 21 9"></polyline>
159
+ <polyline points="9 21 3 21 3 15"></polyline>
160
+ <line x1="21" y1="3" x2="14" y2="10"></line>
161
+ <line x1="3" y1="21" x2="10" y2="14"></line>
162
+ </svg>
163
+ </button>
164
+ </div>
165
+
166
+ <!-- Reveal.js 资源 -->
167
+ <link rel="stylesheet" href="/slides/reveal.css" />
168
+ <link rel="stylesheet" href={`/slides/theme/${theme}.css`} />
169
+ <link rel="stylesheet" href="/slides/plugin/highlight/monokai.css" />
170
+
171
+ <script is:inline src="/slides/reveal.js"></script>
172
+ <script is:inline src="/slides/plugin/markdown/markdown.js"></script>
173
+ <script is:inline src="/slides/plugin/highlight/highlight.js"></script>
174
+
175
+ <script is:inline define:vars={{ slidesId, revealConfig }}>
176
+ (function() {
177
+ function initSlides() {
178
+ const element = document.getElementById(slidesId);
179
+ if (!element || element.dataset.initialized) return;
180
+
181
+ const config = JSON.parse(revealConfig);
182
+
183
+ // 使用 Reveal 构造函数创建独立实例
184
+ const deck = new Reveal(element, {
185
+ hash: config.hash,
186
+ controls: config.controls,
187
+ progress: config.progress,
188
+ center: config.center,
189
+ transition: config.transition,
190
+ slideNumber: config.slideNumber,
191
+ embedded: config.embedded,
192
+ keyboard: config.keyboard,
193
+ overview: config.overview,
194
+ touch: config.touch,
195
+ plugins: [RevealMarkdown, RevealHighlight]
196
+ });
197
+
198
+ deck.initialize();
199
+ element.dataset.initialized = 'true';
200
+ }
201
+
202
+ if (document.readyState === 'loading') {
203
+ document.addEventListener('DOMContentLoaded', initSlides);
204
+ } else {
205
+ // 延迟初始化确保 Reveal.js 已加载
206
+ setTimeout(initSlides, 100);
207
+ }
208
+ })();
209
+ </script>
210
+ ) : (
211
+ <!-- 无内容提示 -->
212
+ <div class="slides-empty">
213
+ <p>请提供 <code>src</code> 属性或内联 Markdown 内容</p>
214
+ </div>
215
+ )}
216
+
217
+ <style>
218
+ /* 嵌入容器 */
219
+ .slides-embed-container,
220
+ .slides-inline-container {
221
+ position: relative;
222
+ width: 100%;
223
+ border-radius: 0.5rem;
224
+ overflow: hidden;
225
+ background: #000;
226
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
227
+ }
228
+
229
+ /* iframe 样式 */
230
+ .slides-iframe {
231
+ width: 100%;
232
+ height: 100%;
233
+ border: none;
234
+ }
235
+
236
+ /* 内联 Reveal 容器 */
237
+ .slides-inline-container .reveal {
238
+ width: 100%;
239
+ height: 100%;
240
+ }
241
+
242
+ /* 全屏按钮 */
243
+ .slides-fullscreen-btn {
244
+ position: absolute;
245
+ top: 10px;
246
+ right: 10px;
247
+ z-index: 30;
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ width: 32px;
252
+ height: 32px;
253
+ background: rgba(0, 0, 0, 0.5);
254
+ border: none;
255
+ border-radius: 4px;
256
+ color: white;
257
+ cursor: pointer;
258
+ opacity: 0;
259
+ transition: opacity 0.3s;
260
+ text-decoration: none;
261
+ }
262
+
263
+ .slides-embed-container:hover .slides-fullscreen-btn,
264
+ .slides-inline-container:hover .slides-fullscreen-btn {
265
+ opacity: 0.8;
266
+ }
267
+
268
+ .slides-fullscreen-btn:hover {
269
+ opacity: 1 !important;
270
+ background: rgba(0, 0, 0, 0.7);
271
+ }
272
+
273
+ /* 空内容提示 */
274
+ .slides-empty {
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ padding: 2rem;
279
+ background: #f3f4f6;
280
+ border-radius: 0.5rem;
281
+ color: #6b7280;
282
+ }
283
+
284
+ .slides-empty code {
285
+ padding: 0.25rem 0.5rem;
286
+ background: #e5e7eb;
287
+ border-radius: 0.25rem;
288
+ font-size: 0.875rem;
289
+ }
290
+
291
+ :global(.dark) .slides-empty {
292
+ background: #1f2937;
293
+ color: #9ca3af;
294
+ }
295
+
296
+ :global(.dark) .slides-empty code {
297
+ background: #374151;
298
+ }
299
+
300
+ /* 内联模式下的 Reveal.js 样式覆盖 */
301
+ .slides-inline-container .reveal .slides {
302
+ text-align: left;
303
+ }
304
+
305
+ .slides-inline-container .reveal .slides section {
306
+ padding: 20px;
307
+ }
308
+
309
+ /* 全屏模式样式 */
310
+ .slides-inline-container .reveal:fullscreen {
311
+ background: #000;
312
+ }
313
+ </style>
@@ -0,0 +1,111 @@
1
+ ---
2
+ /**
3
+ * 自托管视频组件
4
+ *
5
+ * 使用方法:
6
+ * <Video src="/videos/demo.mp4" />
7
+ * <Video src="/videos/demo.mp4" poster="/images/poster.jpg" />
8
+ * <Video src="/videos/demo.webm" type="video/webm" />
9
+ * <Video src="/videos/demo.mp4" autoplay muted loop />
10
+ */
11
+
12
+ interface Props {
13
+ /** 视频源 URL */
14
+ src: string;
15
+ /** 视频类型 (如 video/mp4, video/webm) */
16
+ type?: string;
17
+ /** 封面图片 */
18
+ poster?: string;
19
+ /** 视频标题 */
20
+ title?: string;
21
+ /** 是否自动播放 */
22
+ autoplay?: boolean;
23
+ /** 是否静音 */
24
+ muted?: boolean;
25
+ /** 是否循环播放 */
26
+ loop?: boolean;
27
+ /** 是否显示控制栏 */
28
+ controls?: boolean;
29
+ /** 预加载策略: none, metadata, auto */
30
+ preload?: 'none' | 'metadata' | 'auto';
31
+ /** 自定义宽高比 (默认 16/9) */
32
+ aspectRatio?: string;
33
+ /** 备用视频源列表 */
34
+ sources?: Array<{ src: string; type: string }>;
35
+ }
36
+
37
+ const {
38
+ src,
39
+ type,
40
+ poster,
41
+ title = '视频',
42
+ autoplay = false,
43
+ muted = false,
44
+ loop = false,
45
+ controls = true,
46
+ preload = 'metadata',
47
+ aspectRatio = '16/9',
48
+ sources = []
49
+ } = Astro.props;
50
+
51
+ // 自动检测视频类型
52
+ function getVideoType(videoSrc: string): string {
53
+ const ext = videoSrc.split('.').pop()?.toLowerCase();
54
+ const typeMap: Record<string, string> = {
55
+ mp4: 'video/mp4',
56
+ webm: 'video/webm',
57
+ ogg: 'video/ogg',
58
+ ogv: 'video/ogg',
59
+ mov: 'video/quicktime',
60
+ avi: 'video/x-msvideo',
61
+ mkv: 'video/x-matroska'
62
+ };
63
+ return typeMap[ext || ''] || 'video/mp4';
64
+ }
65
+
66
+ const videoType = type || getVideoType(src);
67
+
68
+ // 合并所有视频源
69
+ const allSources = [
70
+ { src, type: videoType },
71
+ ...sources
72
+ ];
73
+ ---
74
+
75
+ <div class="video-container self-hosted-video" style={`aspect-ratio: ${aspectRatio};`}>
76
+ <video
77
+ {title}
78
+ {poster}
79
+ {autoplay}
80
+ {muted}
81
+ {loop}
82
+ {controls}
83
+ {preload}
84
+ playsinline
85
+ class="w-full h-full object-contain bg-black"
86
+ >
87
+ {allSources.map(source => (
88
+ <source src={source.src} type={source.type} />
89
+ ))}
90
+ <p class="video-fallback">
91
+ 您的浏览器不支持 HTML5 视频。
92
+ <a href={src} download>点击下载视频</a>
93
+ </p>
94
+ </video>
95
+ <div class="video-platform-badge self-hosted">
96
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
97
+ <polygon points="5 3 19 12 5 21 5 3"></polygon>
98
+ </svg>
99
+ <span>Video</span>
100
+ </div>
101
+ </div>
102
+
103
+ <style>
104
+ .self-hosted-video video {
105
+ border-radius: 0.5rem;
106
+ }
107
+
108
+ .self-hosted-video video::-webkit-media-controls-panel {
109
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
110
+ }
111
+ </style>
@@ -0,0 +1,89 @@
1
+ ---
2
+ /**
3
+ * 统一视频播放器组件 - 自动检测视频平台
4
+ *
5
+ * 使用方法:
6
+ * <VideoPlayer src="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />
7
+ * <VideoPlayer src="https://www.bilibili.com/video/BV1xx411c7mD" />
8
+ * <VideoPlayer src="/videos/demo.mp4" />
9
+ */
10
+
11
+ import YouTube from './YouTube.astro';
12
+ import Bilibili from './Bilibili.astro';
13
+ import Video from './Video.astro';
14
+
15
+ interface Props {
16
+ /** 视频源 URL */
17
+ src: string;
18
+ /** 视频标题 */
19
+ title?: string;
20
+ /** 自定义宽高比 */
21
+ aspectRatio?: string;
22
+ /** 是否自动播放 */
23
+ autoplay?: boolean;
24
+ /** 是否静音 */
25
+ muted?: boolean;
26
+ /** 是否循环播放 */
27
+ loop?: boolean;
28
+ /** 封面图片 (仅自托管视频) */
29
+ poster?: string;
30
+ }
31
+
32
+ const {
33
+ src,
34
+ title,
35
+ aspectRatio = '16/9',
36
+ autoplay = false,
37
+ muted = false,
38
+ loop = false,
39
+ poster
40
+ } = Astro.props;
41
+
42
+ // 检测视频平台
43
+ type Platform = 'youtube' | 'bilibili' | 'self-hosted';
44
+
45
+ function detectPlatform(url: string): Platform {
46
+ if (/youtube\.com|youtu\.be/.test(url)) {
47
+ return 'youtube';
48
+ }
49
+ if (/bilibili\.com|b23\.tv/.test(url)) {
50
+ return 'bilibili';
51
+ }
52
+ return 'self-hosted';
53
+ }
54
+
55
+ const platform = detectPlatform(src);
56
+ ---
57
+
58
+ {platform === 'youtube' && (
59
+ <YouTube
60
+ url={src}
61
+ title={title}
62
+ aspectRatio={aspectRatio}
63
+ autoplay={autoplay}
64
+ muted={muted}
65
+ loop={loop}
66
+ />
67
+ )}
68
+
69
+ {platform === 'bilibili' && (
70
+ <Bilibili
71
+ url={src}
72
+ title={title}
73
+ aspectRatio={aspectRatio}
74
+ autoplay={autoplay}
75
+ muted={muted}
76
+ />
77
+ )}
78
+
79
+ {platform === 'self-hosted' && (
80
+ <Video
81
+ src={src}
82
+ title={title}
83
+ aspectRatio={aspectRatio}
84
+ autoplay={autoplay}
85
+ muted={muted}
86
+ loop={loop}
87
+ poster={poster}
88
+ />
89
+ )}