@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.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 (83) hide show
  1. package/README.md +118 -2
  2. package/package.json +70 -5
  3. package/src/components/AlbumCard.astro +73 -0
  4. package/src/components/Analytics.astro +22 -0
  5. package/src/components/BaseLayout.astro +188 -0
  6. package/src/components/Calendar.astro +379 -0
  7. package/src/components/CategoryBar.astro +74 -0
  8. package/src/components/Comment.astro +48 -0
  9. package/src/components/EncryptedContent.astro +42 -0
  10. package/src/components/Footer.astro +31 -0
  11. package/src/components/Header.astro +153 -0
  12. package/src/components/License.astro +29 -0
  13. package/src/components/Live2DWidget.astro +262 -0
  14. package/src/components/MusicPlayer.astro +940 -0
  15. package/src/components/OutdatedCard.astro +53 -0
  16. package/src/components/Pagination.astro +94 -0
  17. package/src/components/PhotoCard.astro +22 -0
  18. package/src/components/PostCard.astro +89 -0
  19. package/src/components/PostNav.astro +40 -0
  20. package/src/components/RecommendedPost.astro +166 -0
  21. package/src/components/SakuraEffect.astro +152 -0
  22. package/src/components/Sidebar.astro +334 -0
  23. package/src/components/Toc.astro +27 -0
  24. package/src/components/analytics/GoogleAnalytics.astro +22 -0
  25. package/src/components/analytics/La51Analytics.astro +52 -0
  26. package/src/components/analytics/MicrosoftClarity.astro +18 -0
  27. package/src/components/analytics/UmamiAnalytics.astro +86 -0
  28. package/src/components/comments/Artalk.astro +51 -0
  29. package/src/components/comments/Disqus.astro +43 -0
  30. package/src/components/comments/Giscus.astro +50 -0
  31. package/src/components/comments/Twikoo.astro +65 -0
  32. package/src/components/comments/Waline.astro +36 -0
  33. package/src/i18n/en.ts +164 -0
  34. package/src/i18n/index.ts +18 -0
  35. package/src/i18n/resolve.ts +49 -0
  36. package/src/i18n/zh-CN.ts +184 -0
  37. package/src/icons.ts +71 -0
  38. package/src/index.ts +411 -0
  39. package/src/plugins/plantuml-encoder.ts +95 -0
  40. package/src/plugins/rehype-diagram.ts +218 -0
  41. package/src/plugins/rehype-ec-vt-rerun.ts +80 -0
  42. package/src/plugins/rehype-email.ts +75 -0
  43. package/src/plugins/rehype-github-card.ts +114 -0
  44. package/src/plugins/rehype.ts +284 -0
  45. package/src/plugins/remark-diagram.ts +78 -0
  46. package/src/plugins/remark-directives.ts +100 -0
  47. package/src/plugins/remark-excerpt.ts +29 -0
  48. package/src/plugins/remark-image-grid.ts +97 -0
  49. package/src/plugins/remark-reading-time.ts +38 -0
  50. package/src/plugins/scripts/diagram-panzoom.js +388 -0
  51. package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
  52. package/src/routes/404.astro +14 -0
  53. package/src/routes/_hydrate.ts +34 -0
  54. package/src/routes/about.astro +25 -0
  55. package/src/routes/api/allPostMeta.json.ts +26 -0
  56. package/src/routes/archive.astro +81 -0
  57. package/src/routes/categories/[category].astro +37 -0
  58. package/src/routes/categories/index.astro +29 -0
  59. package/src/routes/friends.astro +83 -0
  60. package/src/routes/gallery/[album].astro +97 -0
  61. package/src/routes/gallery/index.astro +100 -0
  62. package/src/routes/guestbook.astro +37 -0
  63. package/src/routes/index.astro +47 -0
  64. package/src/routes/og/[...slug].ts +281 -0
  65. package/src/routes/page/[page].astro +53 -0
  66. package/src/routes/posts/[...slug].astro +350 -0
  67. package/src/routes/robots.txt.ts +18 -0
  68. package/src/routes/rss.xml.ts +37 -0
  69. package/src/routes/series/[name].astro +66 -0
  70. package/src/routes/series/index.astro +64 -0
  71. package/src/routes/sponsor.astro +84 -0
  72. package/src/routes/tags/[tag].astro +40 -0
  73. package/src/routes/tags/index.astro +29 -0
  74. package/src/schema.ts +1218 -0
  75. package/src/scripts/app.ts +1041 -0
  76. package/src/scripts/share-poster.ts +468 -0
  77. package/src/styles/global.css +4864 -0
  78. package/src/utils/crypto.ts +46 -0
  79. package/src/utils/gallery.ts +76 -0
  80. package/src/utils/lqip.ts +18 -0
  81. package/src/utils/related.ts +101 -0
  82. package/src/utils.ts +233 -0
  83. package/src/vite.ts +105 -0
@@ -0,0 +1,50 @@
1
+ ---
2
+ /**
3
+ * Giscus 评论实现(orchid-blog components/comment/Giscus.astro 移植)。
4
+ * 官方 Web Component 用法(esm.sh 动态 import,零 npm 依赖,静态输出兼容)。
5
+ * VT 兼容:合并进单个 inline module 并 data-astro-rerun——外部 identical 脚本
6
+ * 换页不重跑,observer 不会重建;await import 幂等(模块缓存,customElements
7
+ * 不会重复 define)。旧 MutationObserver 经 window 句柄清理,防泄漏。
8
+ */
9
+ import config from "virtual:theme/config";
10
+
11
+ const g = config.comment.giscus;
12
+ ---
13
+
14
+ <giscus-widget
15
+ id="comments"
16
+ repo={g.repo}
17
+ repoId={g.repoId}
18
+ category={g.category}
19
+ categoryId={g.categoryId}
20
+ mapping={g.mapping}
21
+ strict={g.strict}
22
+ reactionsEnabled={g.reactionsEnabled}
23
+ emitMetadata={g.emitMetadata}
24
+ inputPosition={g.inputPosition}
25
+ theme="light"
26
+ lang={g.lang}
27
+ loading={g.loading}
28
+ />
29
+
30
+ <script is:inline type="module" data-astro-rerun>
31
+ await import("https://esm.sh/giscus");
32
+
33
+ // 换页重跑时清理旧观察器,避免泄漏
34
+ if (window.__orchidGiscusObserver) window.__orchidGiscusObserver.disconnect();
35
+
36
+ function updateGiscusTheme() {
37
+ const widget = document.querySelector("giscus-widget");
38
+ if (widget) {
39
+ widget.setAttribute(
40
+ "theme",
41
+ document.documentElement.classList.contains("dark") ? "dark" : "light",
42
+ );
43
+ }
44
+ }
45
+ updateGiscusTheme();
46
+
47
+ const observer = new MutationObserver(updateGiscusTheme);
48
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
49
+ window.__orchidGiscusObserver = observer;
50
+ </script>
@@ -0,0 +1,65 @@
1
+ ---
2
+ /**
3
+ * Twikoo 评论实现(orchid-blog components/comment/Twikoo.astro 移植)。
4
+ * path 口径与 Artalk 一致(/posts/<id>、/guestbook,无尾斜杠),迁移后既有评论数据无缝延续。
5
+ * VT 兼容:identical inline 脚本换页不重跑,data-astro-rerun 强制重执行;
6
+ * 脚本加载失败时的 500ms 重试链经 window 句柄清理,防重入叠加。
7
+ */
8
+ import config from "virtual:theme/config";
9
+
10
+ interface Props {
11
+ path: string;
12
+ }
13
+
14
+ const { path } = Astro.props;
15
+ const tw = config.comment.twikoo;
16
+ const twikooConfig = {
17
+ el: "#tcomment",
18
+ envId: tw.envId,
19
+ path,
20
+ };
21
+ ---
22
+
23
+ <div class="relative w-full">
24
+ <div id="tcomment"></div>
25
+
26
+ {tw.cssUrl && <link rel="stylesheet" href={tw.cssUrl} />}
27
+
28
+ <script is:inline src={tw.jsUrl}></script>
29
+
30
+ <script is:inline data-astro-rerun define:vars={{ twikooConfig }}>
31
+ // 换页重跑时清掉上次执行遗留的重试定时器,防叠加
32
+ if (window.__orchidTwikooTimer) {
33
+ clearTimeout(window.__orchidTwikooTimer);
34
+ window.__orchidTwikooTimer = undefined;
35
+ }
36
+
37
+ function initTwikoo() {
38
+ const el = document.getElementById("tcomment");
39
+ if (!el) return;
40
+ if (typeof twikoo === "undefined") {
41
+ // 脚本尚在加载,稍后重试
42
+ window.__orchidTwikooTimer = setTimeout(initTwikoo, 500);
43
+ return;
44
+ }
45
+ el.innerHTML = "";
46
+ twikoo.init(twikooConfig);
47
+ // meta 区浏览量(§4.14):getCommentsCount 填充头部 #twikoo_visitors(与 orchid-blog 口径一致)
48
+ var pv = document.getElementById("twikoo_visitors");
49
+ if (pv) {
50
+ twikoo
51
+ .getCommentsCount({
52
+ envId: twikooConfig.envId,
53
+ urls: [twikooConfig.path],
54
+ includeReply: false,
55
+ })
56
+ .then(function (r) {
57
+ pv.textContent = String((r && r[0] && r[0].count) || 0);
58
+ })
59
+ .catch(function () {});
60
+ }
61
+ }
62
+
63
+ initTwikoo();
64
+ </script>
65
+ </div>
@@ -0,0 +1,36 @@
1
+ ---
2
+ /**
3
+ * Waline 评论实现(orchid-blog components/comment/Waline.astro 移植)。
4
+ * dark: 'html.dark' 由 Waline 自身监听根元素 class,无需 MutationObserver。
5
+ * VT 兼容:identical module 脚本换页不重跑(评论区 DOM 已随 swap 重建),
6
+ * data-astro-rerun 强制重新 init。
7
+ */
8
+ import config from "virtual:theme/config";
9
+
10
+ interface Props {
11
+ path: string;
12
+ }
13
+
14
+ const { path } = Astro.props;
15
+ const w = config.comment.waline;
16
+ const walineConfig = {
17
+ el: "#waline",
18
+ serverURL: w.serverURL,
19
+ path,
20
+ lang: w.lang,
21
+ dark: "html.dark",
22
+ wordLimit: [2, 300],
23
+ ...(w.visitorCount ? { pageview: true } : {}),
24
+ };
25
+ ---
26
+
27
+ <div class="relative w-full">
28
+ <div id="waline"></div>
29
+
30
+ <link rel="stylesheet" href="https://unpkg.com/@waline/client@v3/dist/waline.css" />
31
+
32
+ <script is:inline type="module" data-astro-rerun define:vars={{ walineConfig }}>
33
+ const { init } = await import("https://unpkg.com/@waline/client@v3/dist/waline.js");
34
+ init(walineConfig);
35
+ </script>
36
+ </div>
package/src/i18n/en.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 英文语言包:Record<UiKey, string> 强制与中文键源一一对应,缺译会在编译期报错。
3
+ */
4
+ import type { UiKey } from "./zh-CN.js";
5
+
6
+ const en: Record<UiKey, string> = {
7
+ skipToContent: "Skip to content",
8
+ backToTop: "Back to top",
9
+ mainNav: "Main navigation",
10
+ toggleMode: "Toggle dark mode",
11
+ menu: "Menu",
12
+ palette: "Theme color",
13
+ paletteTitle: "Accent color",
14
+ reset: "Reset",
15
+ hueValue: "Hue {n}",
16
+ pinned: "Pinned",
17
+ wordsCount: "{n} words",
18
+ minutesCount: "{n} min read",
19
+ prevNext: "Previous / Next",
20
+ prevPost: "Previous",
21
+ nextPost: "Next",
22
+ firstPost: "This is the earliest post",
23
+ lastPost: "This is the latest post",
24
+ relatedPosts: "Related Posts",
25
+ randomPosts: "Random Posts",
26
+ smartRecommend: "Smart",
27
+ randomRecommend: "Random",
28
+ noRelatedPosts: "No related posts",
29
+ noRandomPosts: "No random posts",
30
+ licensePrefix: "This post is licensed under",
31
+ licenseSuffix: ". Please credit the author when reposting.",
32
+ toc: "Table of contents",
33
+ tocTitle: "Contents",
34
+ pagination: "Pagination",
35
+ prevPage: "Previous",
36
+ nextPage: "Next",
37
+ pageOf: "Page {current} of {total}",
38
+ closeAnnouncement: "Close announcement",
39
+ closeAd: "Close ad",
40
+ categories: "Categories",
41
+ tags: "Tags",
42
+ friendLinks: "Friends",
43
+ siteInfo: "Site info",
44
+ posts: "Posts",
45
+ running: "Running for",
46
+ daysCount: "{n} days",
47
+ dayPostsCount: "{n} posts",
48
+ heatWeekTip: "Week {w} of {m} · {n}",
49
+ /* §4.47 calendar head / §4.48 tags more entry */
50
+ tagsMore: "… more",
51
+ calendarHome: "This month",
52
+ calendarPick: "Pick year & month",
53
+ layoutSwitch: "Switch list / grid view",
54
+ layoutList: "List view",
55
+ layoutGrid: "Grid view",
56
+ emptyPosts: "No posts yet. Write one in content/posts.",
57
+ pageN: "Page {n}",
58
+ archive: "Archive",
59
+ home: "Home",
60
+ categoryNav: "Category navigation",
61
+ more: "More",
62
+ totalPosts: "{n} posts in total",
63
+ postsInYear: "{n} posts",
64
+ yearUnit: "{n}",
65
+ totalTags: "{n} tags in total",
66
+ tagTitle: "Tag: {tag}",
67
+ totalCategories: "{n} categories in total",
68
+ categoryTitle: "Category: {category}",
69
+ publishedOn: "Published on {date}",
70
+ updatedOn: "Updated on {date}",
71
+ pageviews: "Page views",
72
+ notFound: "Page not found",
73
+ notFoundDesc: "The page does not exist or has been moved.",
74
+ backHome: "Back to home",
75
+ about: "About",
76
+ aboutEmpty: "Write this page in src/content/spec/about.md",
77
+ avatarAlt: "Avatar of {name}",
78
+ noFriends: "No friend links yet",
79
+ applyFriend: "Apply for a link",
80
+ guestbook: "Guestbook",
81
+ guestbookSub: "Leave your trace here",
82
+ commentComingSoon: "Comments are coming soon. Stay tuned.",
83
+ qrAlt: "QR code of {name}",
84
+ goSponsor: "Donate",
85
+ qrPlaceholder: "QR code placeholder",
86
+ sponsorList: "Sponsors",
87
+ noSponsors: "Be the first to sponsor",
88
+ search: "Search",
89
+ searchShortcut: "Press / or Ctrl+K to search",
90
+ searchLoading: "Searching…",
91
+ searchNoResults: "No results found",
92
+ searchUnavailable: "Search index available after build",
93
+ searchTypeSomething: "Type to search",
94
+ galleryPhotos: "{n} photos",
95
+ backToAlbums: "Back to albums",
96
+ searchAlbums: "Search albums…",
97
+ allTags: "All",
98
+ galleryNoAlbums: "No albums yet. Add one in theme config gallery.albums.",
99
+ galleryNoPhotos: "No photos in this album",
100
+ encryptedAlbum: "Encrypted album",
101
+ searchAlbumNoResults: "No matching albums",
102
+ postEncrypted: "Encrypted",
103
+ passwordProtected: "Password Protected",
104
+ passwordProtectedDesc:
105
+ "This content is password protected. Enter the password to view.",
106
+ passwordHint: "Hint",
107
+ passwordPlaceholder: "Enter password",
108
+ passwordSubmit: "Unlock",
109
+ passwordError: "Wrong password, please try again.",
110
+ close: "Close",
111
+ prevImage: "Previous image",
112
+ nextImage: "Next image",
113
+ copied: "Copied",
114
+ fullscreen: "Fullscreen",
115
+ slideshow: "Slideshow",
116
+ thumbs: "Thumbnails",
117
+ sharePoster: "Generate share poster",
118
+ generatingPoster: "Generating poster…",
119
+ copyLink: "Copy link",
120
+ savePoster: "Save poster",
121
+ scanToRead: "Scan to read",
122
+ author: "Author",
123
+ supportTitle: "Support",
124
+ supportDesc: "If this post helped you, consider sponsoring or sharing it.",
125
+ sponsorMe: "Sponsor",
126
+ lastModifiedPrefix: "Last updated on ",
127
+ lastModifiedDaysAgo: ", {days} days ago",
128
+ lastModifiedOutdated: "Some content may be outdated.",
129
+ series: "Series",
130
+ seriesTotal: "{n} series in total",
131
+ seriesPostsCount: "{n} posts",
132
+ seriesUpdated: "Updated on {date}",
133
+ seriesEmpty:
134
+ "No series yet. Add a series field to a post's frontmatter to create one.",
135
+ seriesProgress: "{cur} / {total}",
136
+ seriesCatalog: "Series catalog",
137
+ /* Music player (§4.36) */
138
+ musicOpen: "Music player",
139
+ musicClose: "Collapse player",
140
+ musicPlay: "Play",
141
+ musicPause: "Pause",
142
+ musicPrev: "Previous track",
143
+ musicNext: "Next track",
144
+ musicModeList: "List loop",
145
+ musicModeOne: "Repeat one",
146
+ musicModeRandom: "Shuffle",
147
+ musicLyrics: "Lyrics",
148
+ musicPlaylist: "Playlist",
149
+ musicNoLyrics: "No lyrics",
150
+ musicEmpty: "Playlist is empty",
151
+ musicSeek: "Playback progress",
152
+ musicVolume: "Volume",
153
+ l2dHome: "Home",
154
+ l2dScrollTop: "Back to top",
155
+ l2dSleep: "Take a nap",
156
+ l2dSwitchModel: "Switch model",
157
+ /* Code folding (§4.61): Expressive Code pluginCollapsible option texts (node-side) */
158
+ codeFoldExpand: "Expand code",
159
+ codeFoldCollapse: "Collapse code",
160
+ codeFoldExpanded: "Code block expanded",
161
+ codeFoldCollapsed: "Code block collapsed",
162
+ };
163
+
164
+ export default en;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * i18n 入口(构建期 only):语言在构建时由 themeConfig.lang 确定并注入产物,
3
+ * 不做客户端语言切换。缺译时回退中文键源,保证渲染永不落空。
4
+ * 客户端动态文案(复制按钮/灯箱 aria)经 BaseLayout 输出的 #qy-ui-labels JSON 载体带入。
5
+ *
6
+ * 注意:本模块顶层 import virtual:theme/config,只能被 dev/build 运行期的
7
+ * vite 管线模块(.astro 路由/组件)引用;astro.config 静态 import 链上的
8
+ * node 侧代码(rehype 插件等)请改用 ./resolve.js 的 resolveLabel(lang, key)。
9
+ */
10
+ import config from "virtual:theme/config";
11
+ import { resolveLabel, type UiKey } from "./resolve.js";
12
+
13
+ /** 取 UI 文案;vars 替换 {name} 占位符;缺译回退中文 */
14
+ export function t(key: UiKey, vars?: Record<string, string | number>): string {
15
+ return resolveLabel(config.lang, key, vars);
16
+ }
17
+
18
+ export type { UiKey };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 语言包解析(纯函数,无 virtual:theme/config 依赖)。
3
+ *
4
+ * 独立成模块的原因:astro.config 的静态 import 链(index.ts → plugins/rehype.ts)
5
+ * 在 astro 配置加载期求值,彼时 virtual:theme/config 尚未注册,顶层 import 会直接
6
+ * 报 "Cannot find module"。需要 lang 的调用方(路由/组件走 i18n/index.ts 的 t();
7
+ * node 侧插件走本模块的 resolveLabel + 显式传入的 config.lang)。
8
+ */
9
+
10
+ import en from "./en.js";
11
+ import zhCN, { type UiKey } from "./zh-CN.js";
12
+
13
+ type Pack = Partial<Record<UiKey, string>>;
14
+
15
+ /** 已内置语言包(键为归一化 lang:小写、下划线转连字符) */
16
+ const packs: Record<string, Pack> = {
17
+ en: en,
18
+ "en-us": en,
19
+ "en-gb": en,
20
+ };
21
+
22
+ function norm(lang: string): string {
23
+ return lang.toLowerCase().replace(/_/g, "-");
24
+ }
25
+
26
+ function resolvePack(lang: string): Pack {
27
+ const l = norm(lang);
28
+ if (packs[l]) return packs[l];
29
+ const base = l.split("-")[0];
30
+ if (packs[base]) return packs[base];
31
+ return zhCN;
32
+ }
33
+
34
+ /** 取 UI 文案;vars 替换 {name} 占位符;缺译回退中文键源 */
35
+ export function resolveLabel(
36
+ lang: string,
37
+ key: UiKey,
38
+ vars?: Record<string, string | number>,
39
+ ): string {
40
+ let s: string = resolvePack(lang)[key] ?? zhCN[key];
41
+ if (vars) {
42
+ for (const [k, v] of Object.entries(vars)) {
43
+ s = s.replaceAll(`{${k}}`, String(v));
44
+ }
45
+ }
46
+ return s;
47
+ }
48
+
49
+ export type { UiKey };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * 中文语言包(键源):UiKey 由本文件的键推导。
3
+ * 约定:占位符用 {name} 形式,t(key, vars) 调用处替换。
4
+ * 新增文案先在这里加键,再同步 en.ts(类型会强制检查完整性)。
5
+ */
6
+ const zhCN = {
7
+ /* 全局框架(BaseLayout) */
8
+ skipToContent: "跳到主要内容",
9
+ backToTop: "回到顶部",
10
+ /* Header */
11
+ mainNav: "主导航",
12
+ toggleMode: "切换深浅模式",
13
+ menu: "菜单",
14
+ palette: "调色",
15
+ paletteTitle: "主题色",
16
+ reset: "重置",
17
+ hueValue: "色相 {n}",
18
+ /* PostCard / 文章元信息 */
19
+ pinned: "置顶",
20
+ wordsCount: "{n} 字",
21
+ minutesCount: "约 {n} 分钟",
22
+ /* PostNav */
23
+ prevNext: "上下篇",
24
+ prevPost: "上一篇",
25
+ nextPost: "下一篇",
26
+ firstPost: "已经是最早的一篇",
27
+ lastPost: "已经是最新的一篇",
28
+ /* 相关/随机文章推荐(M3b-b9):noRandomPosts 经 data-empty-html 带入客户端 */
29
+ relatedPosts: "相关文章",
30
+ randomPosts: "随机文章",
31
+ smartRecommend: "智能推荐",
32
+ randomRecommend: "随机推荐",
33
+ noRelatedPosts: "暂无相关文章",
34
+ noRandomPosts: "暂无随机文章",
35
+ /* License */
36
+ licensePrefix: "本文采用",
37
+ licenseSuffix: "许可,转载请注明出处与作者。",
38
+ /* Toc */
39
+ toc: "目录",
40
+ tocTitle: "目 录",
41
+ /* Pagination */
42
+ pagination: "分页",
43
+ prevPage: "上一页",
44
+ nextPage: "下一页",
45
+ pageOf: "第 {current} / {total} 页",
46
+ /* Sidebar */
47
+ closeAnnouncement: "关闭公告",
48
+ closeAd: "关闭广告",
49
+ categories: "分类",
50
+ tags: "标签",
51
+ friendLinks: "友链",
52
+ siteInfo: "站点信息",
53
+ posts: "文章",
54
+ running: "已运行",
55
+ daysCount: "{n} 天",
56
+ dayPostsCount: "{n} 篇",
57
+ heatWeekTip: "{m}第{w}周 · {n}",
58
+ /* §4.47 日历头改造 / §4.48 标签卡更多入口 */
59
+ tagsMore: "…更多",
60
+ calendarHome: "回到本月",
61
+ calendarPick: "选择年月",
62
+ /* 列表 / 聚合页 */
63
+ /* 列表形态切换(§4.34) */
64
+ layoutSwitch: "切换列表 / 网格视图",
65
+ layoutList: "列表视图",
66
+ layoutGrid: "网格视图",
67
+ emptyPosts: "还没有文章,去 content/posts 里写一篇吧。",
68
+ pageN: "第 {n} 页",
69
+ archive: "归档",
70
+ home: "首页",
71
+ categoryNav: "分类导航",
72
+ more: "更多",
73
+ totalPosts: "共 {n} 篇文章",
74
+ postsInYear: "{n} 篇",
75
+ yearUnit: "{n} 年",
76
+ totalTags: "共 {n} 个标签",
77
+ tagTitle: "标签:{tag}",
78
+ totalCategories: "共 {n} 个分类",
79
+ categoryTitle: "分类:{category}",
80
+ publishedOn: "{date} 发布",
81
+ updatedOn: "{date} 更新",
82
+ pageviews: "浏览量",
83
+ /* 404 */
84
+ notFound: "页面不存在",
85
+ notFoundDesc: "页面不存在,或已被移动到别处。",
86
+ backHome: "返回首页",
87
+ /* 独立页 */
88
+ about: "关于",
89
+ aboutEmpty: "在 src/content/spec/about.md 中编写本页内容",
90
+ avatarAlt: "{name} 的头像",
91
+ noFriends: "还没有友链",
92
+ applyFriend: "申请友链",
93
+ guestbook: "留言板",
94
+ guestbookSub: "欢迎留下你的足迹",
95
+ commentComingSoon: "评论区即将开放,敬请期待。",
96
+ qrAlt: "{name} 收款码",
97
+ goSponsor: "前往打赏",
98
+ qrPlaceholder: "收款码占位",
99
+ sponsorList: "打赏名单",
100
+ noSponsors: "还没有打赏,快来做第一个吧",
101
+ /* 搜索(M3b-b6):search 用于 placeholder(服务端渲染),其余经 #qy-ui-labels 带入客户端 */
102
+ search: "搜索",
103
+ searchShortcut: "按 / 或 Ctrl+K 快速搜索",
104
+ searchLoading: "搜索中…",
105
+ searchNoResults: "没有找到相关结果",
106
+ searchUnavailable: "搜索索引需构建后可用",
107
+ searchTypeSomething: "输入关键词开始搜索",
108
+ /* 画廊(M3b-b8):title/description 走主题配置,这里只放 UI 通用文案 */
109
+ galleryPhotos: "{n} 张照片",
110
+ backToAlbums: "返回相册",
111
+ searchAlbums: "搜索相册…",
112
+ allTags: "全部",
113
+ galleryNoAlbums: "还没有相册,在主题配置 gallery.albums 中添加。",
114
+ galleryNoPhotos: "相册里还没有照片",
115
+ encryptedAlbum: "加密相册",
116
+ searchAlbumNoResults: "没有匹配的相册",
117
+ /* 加密文章(M3b-b7):postEncrypted 用于列表/文章 meta 标记,其余构建期渲染进锁 UI */
118
+ postEncrypted: "已加密",
119
+ passwordProtected: "密码保护",
120
+ passwordProtectedDesc: "此内容已被密码保护,请输入密码查看。",
121
+ passwordHint: "提示",
122
+ passwordPlaceholder: "请输入密码",
123
+ passwordSubmit: "解锁",
124
+ passwordError: "密码错误,请重试。",
125
+ /* 客户端动态文案(构建期经 BaseLayout 的 #qy-ui-labels JSON 载体带入;close/prevImage/nextImage/fullscreen/slideshow/thumbs 供 PhotoSwipe,§4.69-§4.71) */
126
+ close: "关闭",
127
+ prevImage: "上一张",
128
+ nextImage: "下一张",
129
+ copied: "已复制",
130
+ fullscreen: "全屏",
131
+ slideshow: "自动播放",
132
+ thumbs: "缩略图条",
133
+ /* 分享海报(M3b-b9.6) */
134
+ sharePoster: "生成分享海报",
135
+ generatingPoster: "海报生成中…",
136
+ copyLink: "复制链接",
137
+ savePoster: "保存海报",
138
+ scanToRead: "扫码阅读全文",
139
+ author: "作者",
140
+ /* 文章页支持卡(打赏入口 + 分享海报) */
141
+ supportTitle: "支持一下",
142
+ supportDesc: "如果这篇文章对你有帮助,欢迎打赏或分享。",
143
+ sponsorMe: "打赏支持",
144
+ /* 文章过时提醒 */
145
+ lastModifiedPrefix: "最后更新于 ",
146
+ lastModifiedDaysAgo: ",距今已过 {days} 天",
147
+ lastModifiedOutdated: "部分内容可能已过时,请注意信息时效。",
148
+ /* 专题(§4.31):frontmatter series 聚合页与文章页专题导航 */
149
+ series: "专题",
150
+ seriesTotal: "共 {n} 个专题",
151
+ seriesPostsCount: "{n} 篇",
152
+ seriesUpdated: "更新于 {date}",
153
+ seriesEmpty: "还没有专题,在文章 frontmatter 中填写 series 字段即可创建。",
154
+ seriesProgress: "{cur} / {total}",
155
+ seriesCatalog: "专题目录",
156
+ /* 音乐播放器(§4.36):data-labels 经组件 data-labels 属性带入客户端 */
157
+ musicOpen: "音乐播放器",
158
+ musicClose: "收起播放器",
159
+ musicPlay: "播放",
160
+ musicPause: "暂停",
161
+ musicPrev: "上一首",
162
+ musicNext: "下一首",
163
+ musicModeList: "列表循环",
164
+ musicModeOne: "单曲循环",
165
+ musicModeRandom: "随机播放",
166
+ musicLyrics: "歌词",
167
+ musicPlaylist: "播放列表",
168
+ musicNoLyrics: "暂无歌词",
169
+ musicEmpty: "播放列表为空",
170
+ musicSeek: "播放进度",
171
+ musicVolume: "音量",
172
+ l2dHome: "回到主页",
173
+ l2dScrollTop: "回到顶部",
174
+ l2dSleep: "休息一下",
175
+ l2dSwitchModel: "换个模型",
176
+ /* 代码折叠(§4.61):Expressive Code pluginCollapsible 选项文案(node 侧消费) */
177
+ codeFoldExpand: "展开代码",
178
+ codeFoldCollapse: "收起代码",
179
+ codeFoldExpanded: "代码已展开",
180
+ codeFoldCollapsed: "代码已收起",
181
+ } as const;
182
+
183
+ export type UiKey = keyof typeof zhCN;
184
+ export default zhCN;
package/src/icons.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 社交图标解析(§4.65 标准化迁移):自研 SVG 表退役,改走 astro-icon + Iconify
3
+ * 标准生态——构建期内联渲染、无运行时请求,语义与参考项目一致。
4
+ *
5
+ * icon 字段解析优先级(Sidebar 资料卡):
6
+ * 1. "<" 开头 → 内联 SVG 字符串(§4.59 向后兼容,调用方 set:html 直出)
7
+ * 2. 含 ":" → Iconify 全名(如 "tabler:home"),直接透传 <Icon>;
8
+ * 站点装任意 @iconify-json/* 集合包即可扩展
9
+ * 3. 内置简名 → 查 NAMED_ICONS 映射表(下方)
10
+ * 4. 标准库未收录品牌 → 查 FALLBACK_ICONS 内联兜底(仅 douyin)
11
+ * 5. 未命中 → 返回 null,调用方跳过图标只渲染文字
12
+ *
13
+ * 简名映射的数据来源与许可:
14
+ * - 线性图标 → lucide(ISC):github/email/rss,与原内置 SVG 同源,视觉零变化
15
+ * - 品牌图标 → simple-icons(CC0):微博在其中的规范名为 sinaweibo
16
+ */
17
+ export interface ResolvedIcon {
18
+ /** Iconify 全名("collection:name"),交给 astro-icon 的 <Icon> 渲染 */
19
+ name?: string;
20
+ /** 标准库未收录品牌的内联 SVG 兜底,调用方 set:html 直出 */
21
+ html?: string;
22
+ }
23
+
24
+ /** 内置简名 → Iconify 全名 */
25
+ const NAMED_ICONS: Record<string, string> = {
26
+ github: "lucide:github",
27
+ email: "lucide:mail",
28
+ rss: "lucide:rss",
29
+ qq: "simple-icons:qq",
30
+ wechat: "simple-icons:wechat",
31
+ weibo: "simple-icons:sinaweibo",
32
+ bilibili: "simple-icons:bilibili",
33
+ xiaohongshu: "simple-icons:xiaohongshu",
34
+ x: "simple-icons:x",
35
+ telegram: "simple-icons:telegram",
36
+ youtube: "simple-icons:youtube",
37
+ instagram: "simple-icons:instagram",
38
+ linkedin: "simple-icons:linkedin",
39
+ discord: "simple-icons:discord",
40
+ steam: "simple-icons:steam",
41
+ zhihu: "simple-icons:zhihu",
42
+ gitee: "simple-icons:gitee",
43
+ juejin: "simple-icons:juejin",
44
+ csdn: "simple-icons:csdn",
45
+ };
46
+
47
+ /** 常用别名 → 规范名(如微信拼音/旧称) */
48
+ const ALIASES: Record<string, string> = { weixin: "wechat", twitter: "x" };
49
+
50
+ // simple-icons 未收录抖音官方形(tiktok 为国际版),保留 CC0 单色内联兜底;
51
+ // 上游收录后可将该条并入 NAMED_ICONS
52
+ const FALLBACK_ICONS: Record<string, string> = {
53
+ douyin:
54
+ '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path fill="currentColor" d="M12.525.02c1.31-.02 2.61-.01 3.91-.02c.08 1.53.63 3.09 1.75 4.17c1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97c-.57-.26-1.1-.59-1.62-.93c-.01 2.92.01 5.84-.02 8.75c-.08 1.4-.54 2.79-1.35 3.94c-1.31 1.92-3.58 3.17-5.91 3.21c-1.43.08-2.86-.31-4.08-1.03c-2.02-1.19-3.44-3.37-3.65-5.71c-.02-.5-.03-1-.01-1.49c.18-1.9 1.12-3.72 2.58-4.96c1.66-1.44 3.98-2.13 6.15-1.72c.02 1.48-.04 2.96-.04 4.44c-.99-.32-2.15-.23-3.02.37c-.63.41-1.11 1.04-1.36 1.75c-.21.51-.15 1.07-.14 1.61c.24 1.64 1.82 3.02 3.5 2.87c1.12-.01 2.19-.66 2.77-1.61c.19-.33.4-.67.41-1.06c.1-1.79.06-3.57.07-5.36c.01-4.03-.01-8.05.02-12.07"/></svg>',
55
+ };
56
+
57
+ /**
58
+ * 按图标名解析(§4.59/§4.65):trim + 小写 + 别名归一,
59
+ * 命中返回 { name }(Iconify 全名)或 { html }(内联兜底),
60
+ * 未命中返回 null(调用方跳过图标、只渲染文字)。
61
+ */
62
+ export function resolveThemeIcon(raw: string): ResolvedIcon | null {
63
+ const key = raw.trim().toLowerCase();
64
+ if (!key) return null;
65
+ // Iconify 全名直接透传(扩展点)
66
+ if (key.includes(":")) return { name: key };
67
+ const canonical = ALIASES[key] ?? key;
68
+ if (NAMED_ICONS[canonical]) return { name: NAMED_ICONS[canonical] };
69
+ if (FALLBACK_ICONS[canonical]) return { html: FALLBACK_ICONS[canonical] };
70
+ return null;
71
+ }