@windcoder/astro-theme-orchid 0.2.1 → 0.4.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 (41) hide show
  1. package/README.md +5 -3
  2. package/package.json +4 -2
  3. package/scripts/check-friends.mjs +227 -0
  4. package/scripts/generate-lqips.mjs +234 -20
  5. package/scripts/migrate-posts.mjs +375 -0
  6. package/src/components/AlbumCard.astro +4 -1
  7. package/src/components/Analytics.astro +3 -0
  8. package/src/components/BaseLayout.astro +222 -39
  9. package/src/components/DisplaySettings.astro +383 -0
  10. package/src/components/Header.astro +56 -2
  11. package/src/components/MusicPlayer.astro +45 -5
  12. package/src/components/MusicWidget.astro +265 -0
  13. package/src/components/PhotoCard.astro +13 -1
  14. package/src/components/PostCard.astro +4 -1
  15. package/src/components/SakuraEffect.astro +126 -28
  16. package/src/components/Sidebar.astro +28 -7
  17. package/src/components/analytics/WebVitals.astro +72 -0
  18. package/src/i18n/en.ts +42 -0
  19. package/src/i18n/ja.ts +213 -0
  20. package/src/i18n/ko.ts +212 -0
  21. package/src/i18n/resolve.ts +10 -1
  22. package/src/i18n/ru.ts +213 -0
  23. package/src/i18n/zh-CN.ts +43 -0
  24. package/src/i18n/zh-TW.ts +208 -0
  25. package/src/index.ts +23 -0
  26. package/src/plugins/rehype.ts +76 -4
  27. package/src/routes/bangumi.astro +110 -0
  28. package/src/routes/friends.astro +10 -2
  29. package/src/routes/manifest.webmanifest.ts +33 -0
  30. package/src/routes/page/[page].astro +3 -0
  31. package/src/routes/search.astro +24 -0
  32. package/src/routes/sponsor.astro +17 -4
  33. package/src/routes/sw.js.ts +127 -0
  34. package/src/schema.ts +446 -45
  35. package/src/scripts/app.ts +805 -37
  36. package/src/styles/global.css +939 -18
  37. package/src/utils/gallery.ts +3 -1
  38. package/src/utils/image.ts +79 -0
  39. package/src/utils/lqip.ts +21 -12
  40. package/src/utils/thumb.ts +26 -0
  41. package/src/vite.ts +95 -8
package/src/schema.ts CHANGED
@@ -37,6 +37,206 @@ const sidebarWidgetBase = z.object({
37
37
  specificConfig: z.object({}).default({}),
38
38
  });
39
39
 
40
+ /**
41
+ * 统一壁纸体系(§4.98):原 banner(§4.42)+ background(§4.92)合并为单段配置,
42
+ * 四种模式 html[data-wallpaper-mode] 驱动:banner 横幅 / fullscreen 全屏首屏 /
43
+ * overlay 覆盖透明垫底 / none 纯色不渲染。
44
+ * 图源遵循三格式约定(§4.97):"/" 开头按 public 路径原样输出;http(s)://、//、data:
45
+ * 外链原样输出;其余相对路径相对站点 src/ 目录解析(构建期查表,支持 assets 内图片)。
46
+ * 提取为模块级 const 以便使用函数默认值 .default(() => wallpaperSchema.parse({})):
47
+ * Zod 的 .default(x) 在输入缺失时直接取 x、不做二次 parse(v3/v4 行为一致),
48
+ * 本段嵌套深达 4 层,最外层再写一份完整字面量维护成本过高;改用 parse({}) 在
49
+ * 运行期递归补齐全部内层默认值(内层各段仍按惯例写显式完整字面量)。
50
+ */
51
+ const wallpaperSchema = z.object({
52
+ /** 壁纸模式:banner 横幅 / fullscreen 全屏首屏 / overlay 覆盖透明垫底 / none 纯色 */
53
+ mode: z.enum(["banner", "fullscreen", "overlay", "none"]).default("banner"),
54
+ /**
55
+ * 是否允许访客在导航栏循环切换壁纸模式(banner→fullscreen→overlay→none),
56
+ * 选择持久化到 localStorage('qy-wallpaper'),优先于本配置的 mode。
57
+ */
58
+ switchable: z.boolean().default(false),
59
+ /** 视频壁纸播放器总开关:false 时不渲染 video 元素与播放控制按钮 */
60
+ playerEnable: z.boolean().default(false),
61
+ /** 壁纸图源与视频源(三格式约定见 §4.97) */
62
+ src: z
63
+ .object({
64
+ /** 桌面端图源:多图数组构建期随机取一或开启轮播;空数组 = 无壁纸图 */
65
+ desktop: z.array(z.string()).default([]),
66
+ /** 移动端图源(<768px):留空数组则移动端复用 desktop 层全宽显示 */
67
+ mobile: z.array(z.string()).default([]),
68
+ /** 视频源列表(playerEnable 开启时生效) */
69
+ playerUrl: z.array(z.string()).default([]),
70
+ })
71
+ .default({ desktop: [], mobile: [], playerUrl: [] }),
72
+ /** 各模式共用的行为配置 */
73
+ common: z
74
+ .object({
75
+ /** 壁纸蒙层不透明度(0-1),压暗壁纸保证前景可读 */
76
+ dimOpacity: z.number().min(0).max(1).default(0.2),
77
+ /** 暗色模式蒙层不透明度(0-1),暗色下压更暗 */
78
+ darkDimOpacity: z.number().min(0).max(1).default(0.65),
79
+ /** 视频播放顺序:order 按列表顺序 / random 随机 */
80
+ playerMode: z.enum(["order", "random"]).default("random"),
81
+ /** 首页壁纸文案:banner/fullscreen 叠加渲染在壁纸上;无壁纸时复用为独立 hero 区块 */
82
+ homeText: z
83
+ .object({
84
+ /** 文案总开关:false 时标题与副句都不渲染 */
85
+ enable: z.boolean().default(true),
86
+ /** 主标题,<em> 标签内文字以强调色渲染;留空不渲染 */
87
+ title: z.string().default(""),
88
+ /** 主标题字号(css 长度) */
89
+ titleSize: z.string().default("3.8rem"),
90
+ /** 副句:单条或多条(打字机逐条打字;关闭打字机时多条构建期随机取一条静态展示) */
91
+ subtitle: z.union([z.string(), z.array(z.string())]).default([]),
92
+ /** 副句字号(css 长度) */
93
+ subtitleSize: z.string().default("1.5rem"),
94
+ /** 打字机效果(作用于副句) */
95
+ typewriter: z
96
+ .object({
97
+ /** 是否启用打字机(false 时副句静态展示) */
98
+ enable: z.boolean().default(true),
99
+ /** 打字速度 ms/字素 */
100
+ speed: z.number().int().min(10).default(100),
101
+ /** 删除速度 ms/字素 */
102
+ deleteSpeed: z.number().int().min(10).default(50),
103
+ /** 打完一条后的停留 ms */
104
+ pauseTime: z.number().int().min(0).default(2000),
105
+ })
106
+ .default({
107
+ enable: true,
108
+ speed: 100,
109
+ deleteSpeed: 50,
110
+ pauseTime: 2000,
111
+ }),
112
+ })
113
+ .default({
114
+ enable: true,
115
+ title: "",
116
+ titleSize: "3.8rem",
117
+ subtitle: [],
118
+ subtitleSize: "1.5rem",
119
+ typewriter: {
120
+ enable: true,
121
+ speed: 100,
122
+ deleteSpeed: 50,
123
+ pauseTime: 2000,
124
+ },
125
+ }),
126
+ /** 导航栏随壁纸的透明形态 */
127
+ navbar: z
128
+ .object({
129
+ /**
130
+ * semi 顶部半透明+滚动后实底 / semifull 顶部全透明+滚动后半透明 /
131
+ * full 恒全透明;壁纸不可见(none 模式或 homeOnly 下非首页)时强制实底
132
+ */
133
+ transparentMode: z.enum(["semi", "semifull", "full"]).default("semi"),
134
+ /** 透明态是否启用背景模糊 */
135
+ enableBlur: z.boolean().default(true),
136
+ /** 透明态背景模糊半径 px */
137
+ blur: z.number().min(0).max(20).default(5),
138
+ })
139
+ .default({ transparentMode: "semi", enableBlur: true, blur: 5 }),
140
+ /** 底部波浪装饰:true 全端开启 / false 关闭 / 对象分端控制(默认移动端关闭参考站惯例) */
141
+ waves: z
142
+ .union([
143
+ z.boolean(),
144
+ z.object({
145
+ desktop: z.boolean().default(true),
146
+ mobile: z.boolean().default(true),
147
+ }),
148
+ ])
149
+ .default(false),
150
+ /** 底部渐变装饰:true 全端开启 / false 关闭 / 对象分端控制 + 高度 */
151
+ gradient: z
152
+ .union([
153
+ z.boolean(),
154
+ z.object({
155
+ desktop: z.boolean().default(true),
156
+ mobile: z.boolean().default(true),
157
+ /** 渐变层高度(css 长度) */
158
+ height: z.string().default("10%"),
159
+ }),
160
+ ])
161
+ .default(false),
162
+ /** 多图轮播(单图无效果);视频播放中自动暂停轮播 */
163
+ carousel: z
164
+ .object({
165
+ /**
166
+ * false:构建期随机取一张(零 JS);true:全列表渲染 + JS 定时轮换。
167
+ * 轮播仅在 banner/fullscreen/overlay 三种图片模式生效。
168
+ */
169
+ enable: z.boolean().default(false),
170
+ /** 轮换间隔 ms */
171
+ interval: z.number().int().min(1000).default(5000),
172
+ /** 过渡效果:fade 淡入淡出 / zoom 缩放 / slide 滑动 / kenburns 缓慢推近 */
173
+ transitionEffect: z
174
+ .enum(["fade", "zoom", "slide", "kenburns"])
175
+ .default("fade"),
176
+ })
177
+ .default({ enable: false, interval: 5000, transitionEffect: "fade" }),
178
+ })
179
+ .default({
180
+ dimOpacity: 0.2,
181
+ darkDimOpacity: 0.65,
182
+ playerMode: "random",
183
+ homeText: {
184
+ enable: true,
185
+ title: "",
186
+ titleSize: "3.8rem",
187
+ subtitle: [],
188
+ subtitleSize: "1.5rem",
189
+ typewriter: {
190
+ enable: true,
191
+ speed: 100,
192
+ deleteSpeed: 50,
193
+ pauseTime: 2000,
194
+ },
195
+ },
196
+ navbar: { transparentMode: "semi", enableBlur: true, blur: 5 },
197
+ waves: false,
198
+ gradient: false,
199
+ carousel: { enable: false, interval: 5000, transitionEffect: "fade" },
200
+ }),
201
+ /** banner 模式专属(横幅形态) */
202
+ banner: z
203
+ .object({
204
+ /** 横幅高度(vh),默认 60:壁纸从导航栏顶铺满至分类导航条处渐隐,非窄条 */
205
+ height: z.number().int().min(10).max(90).default(60),
206
+ /** true 仅首页显示;false 全站显示(overlay 天然全站,忽略此项) */
207
+ homeOnly: z.boolean().default(true),
208
+ /** 图片 object-position(center / center 30% 等) */
209
+ position: z.string().default("center"),
210
+ })
211
+ .default({ height: 60, homeOnly: true, position: "center" }),
212
+ /** fullscreen 模式专属 */
213
+ fullscreen: z
214
+ .object({
215
+ /** 图片 object-position(center / center 30% 等) */
216
+ position: z.string().default("center"),
217
+ /**
218
+ * 全屏布局(§4.103):classic 经典 = 滚下后正文区纯色背景(默认,旧行为);
219
+ * hero = 滚下后正文区无标题文字,壁纸 fixed 铺满 + 模糊渐变过渡,
220
+ * 卡片浮于模糊壁纸上(复用 overlay 的 blur/cardOpacity 参数)
221
+ */
222
+ layout: z.enum(["classic", "hero"]).default("classic"),
223
+ })
224
+ .default({ position: "center", layout: "classic" }),
225
+ /** overlay 模式专属:fixed 垫底,正文卡片半透明透出壁纸 */
226
+ overlay: z
227
+ .object({
228
+ /** 壁纸层 z-index(默认 -1,位于 body 背景之上、内容之下) */
229
+ zIndex: z.number().default(-1),
230
+ /** 壁纸整体不透明度(0-1),配合蒙层控制透出强度 */
231
+ opacity: z.number().min(0).max(1).default(0.8),
232
+ /** 壁纸模糊半径 px(0-20),模糊时自动轻微放大防止边缘露底 */
233
+ blur: z.number().min(0).max(20).default(0),
234
+ /** 正文卡片背景不透明度(0-1):越小壁纸透出越多 */
235
+ cardOpacity: z.number().min(0).max(1).default(0.5),
236
+ })
237
+ .default({ zIndex: -1, opacity: 0.8, blur: 0, cardOpacity: 0.5 }),
238
+ });
239
+
40
240
  /**
41
241
  * 主题配置 schema(astro.config.ts 中 defineTheme({...}) 的入参)
42
242
  * 后续新增配置项一律给 default,保证老站点升级后无需改动配置
@@ -54,7 +254,7 @@ export const themeConfigSchema = z.object({
54
254
  * 文章页由 frontmatter tags 覆盖。留空则不输出该 meta。
55
255
  */
56
256
  keywords: z.array(z.string()).default([]),
57
- /** 导航栏 Logo(§4.20):标题左侧图片 URL(public 路径或外链),留空只渲染文字标题 */
257
+ /** 导航栏 Logo(§4.20):三格式图源(§4.97:public / 外链原样,相对路径相对站点 src/ 解析),留空只渲染文字标题 */
58
258
  logo: z.string().default(""),
59
259
  /** 作者名 */
60
260
  author: z.string().default(""),
@@ -76,37 +276,73 @@ export const themeConfigSchema = z.object({
76
276
  */
77
277
  pageWidth: z.number().int().min(800).max(1920).default(1600),
78
278
  /**
79
- * 首页 banner(§4.42,hero+壁纸合并单配置):enabled 总开关,false 时图与文案都不渲染。
80
- * src 配置时渲染 Header 下方下沉横幅(数组构建期随机选一张,零 JS)+ 黑蒙层;
81
- * quote 配置时渲染 hero 文案——有 banner 叠加其上,无 banner 为列表上方独立区块。
279
+ * 统一壁纸体系(§4.98,breaking:替代原 banner §4.42 + background §4.92 两段)。
280
+ * banner 横幅 / fullscreen 全屏 / overlay 覆盖透明 / none 纯色四模式,
281
+ * 详见模块级 wallpaperSchema 注释。函数默认值在 wallpaper 键整体缺失时
282
+ * 递归补齐全部内层默认值。
82
283
  */
83
- banner: z
284
+ wallpaper: wallpaperSchema.default(() => wallpaperSchema.parse({})),
285
+ /**
286
+ * 番组追番页(§4.93):构建期从 bgm.tv v0 API 拉取用户「在看」动画收藏,
287
+ * 渲染静态 /bangumi 页。enabled 与 userId 同时满足才注入路由(默认关)。
288
+ */
289
+ bangumi: z
84
290
  .object({
85
- /** 总开关:false 时即使配了 src/quote 也不渲染 */
86
- enabled: z.boolean().default(true),
87
- /** 图源(public 路径或外链):单图 URL 或多图数组(构建期随机取一);不配置则无图 */
88
- src: z.union([z.string(), z.array(z.string())]).optional(),
89
- /** banner 高度(vh) */
90
- height: z.number().int().min(10).max(90).default(32),
91
- /** 黑蒙层不透明度(0-1),深浅模式同色,保证文字可读 */
92
- dimOpacity: z.number().min(0).max(1).default(0.2),
93
- /** 图片 object-position(center / center 30% 等) */
94
- position: z.string().default("center"),
95
- /** true 仅首页显示,false 全站显示 */
96
- homeOnly: z.boolean().default(true),
97
- /** 主句,<em> 标签内文字会以强调色渲染;留空不渲染 hero 文案 */
98
- quote: z.string().default(""),
99
- /** 副句 */
100
- sub: z.string().default(""),
291
+ /** 总开关:与 userId 同时非空才生成 /bangumi 路由(默认关) */
292
+ enabled: z.boolean().default(false),
293
+ /** bgm.tv 用户名(https://bgm.tv/user/{username} 中的 username) */
294
+ userId: z.string().default(""),
295
+ /** 页面标题 */
296
+ title: z.string().default("追番"),
297
+ /** 页面副标题(留空不渲染) */
298
+ description: z.string().default(""),
299
+ })
300
+ .default({ enabled: false, userId: "", title: "追番", description: "" }),
301
+ /**
302
+ * PWA(§4.95):构建期产出 /manifest.webmanifest 与 /sw.js(手写 Service
303
+ * Worker,零运行时依赖),可安装 + 离线兜底。enabled=false 时两路由均不
304
+ * 注入,head 无 manifest 链接与注册脚本,零痕迹。
305
+ */
306
+ pwa: z
307
+ .object({
308
+ /** 总开关:false 时不生成 manifest 与 Service Worker(默认关) */
309
+ enabled: z.boolean().default(false),
310
+ /** 安装后的应用名:留空回落站点 title */
311
+ name: z.string().default(""),
312
+ /** 安装后启动器短名:留空回落 name */
313
+ shortName: z.string().default(""),
314
+ /** 应用描述:留空回落站点 description */
315
+ description: z.string().default(""),
316
+ /** 清单 theme_color:安装 UI / 标题栏着色(独立于页面 theme-color meta) */
317
+ themeColor: z.string().default("#f6f4ef"),
318
+ /** 清单 background_color:启动画面底色 */
319
+ backgroundColor: z.string().default("#f6f4ef"),
320
+ /** 显示形态:standalone 独立应用窗口(默认)/ fullscreen / minimal-ui / browser */
321
+ display: z
322
+ .enum(["standalone", "fullscreen", "minimal-ui", "browser"])
323
+ .default("standalone"),
324
+ /** 图标清单(public 下路径):至少 192x192 + 512x512 各一张才满足可安装性 */
325
+ icons: z
326
+ .array(
327
+ z.object({
328
+ src: z.string(),
329
+ sizes: z.string().default("512x512"),
330
+ type: z.string().default("image/png"),
331
+ /** maskable:安装后按平台形状裁切的容错图标,可与 any 各配一张 */
332
+ purpose: z.enum(["any", "maskable"]).default("any"),
333
+ }),
334
+ )
335
+ .default([]),
101
336
  })
102
337
  .default({
103
- enabled: true,
104
- height: 32,
105
- dimOpacity: 0.2,
106
- position: "center",
107
- homeOnly: true,
108
- quote: "",
109
- sub: "",
338
+ enabled: false,
339
+ name: "",
340
+ shortName: "",
341
+ description: "",
342
+ themeColor: "#f6f4ef",
343
+ backgroundColor: "#f6f4ef",
344
+ display: "standalone",
345
+ icons: [],
110
346
  }),
111
347
  /**
112
348
  * hue 调色面板(M3b-b5):导航栏调色按钮 + 预设色板 + hue 滑杆。
@@ -170,7 +406,7 @@ export const themeConfigSchema = z.object({
170
406
  */
171
407
  profile: z
172
408
  .object({
173
- /** 头像图 URL(留空渲染昵称首字 fallback) */
409
+ /** 头像图(三格式图源 §4.97;留空渲染昵称首字 fallback) */
174
410
  avatar: z.string().default(""),
175
411
  name: z.string().default(""),
176
412
  bio: z.string().default(""),
@@ -289,6 +525,73 @@ export const themeConfigSchema = z.object({
289
525
  * 切换后以 localStorage('qy-mode') 为准。
290
526
  */
291
527
  colorMode: z.enum(["light", "dark", "system"]).default("system"),
528
+ /**
529
+ * 暗色模式适配(§4.89):adaptExternalImages 开启后,暗色下对外链内容图
530
+ * (data-external 标记,白底截图/图表易刺眼)施加亮度滤镜压暗;
531
+ * 本地图与 .no-adapt 单图豁免。默认关闭保持中性。
532
+ */
533
+ darkMode: z
534
+ .object({
535
+ adaptExternalImages: z.boolean().default(false),
536
+ })
537
+ .default({ adaptExternalImages: false }),
538
+ /**
539
+ * 显示设置面板(§4.103,对齐 Firefly):导航栏齿轮入口的三 Tab 浮层
540
+ * (外观/壁纸/特效),集中管理主题色、壁纸模式、全屏布局、壁纸设置、
541
+ * 透明设置与樱花特效的访客级开关,选择持久化 localStorage。
542
+ * enable=false(默认)不渲染齿轮与面板,维持 Header 独立调色板/壁纸切换按钮。
543
+ * 各 switchable 控制面板内对应项是否对访客可见;无可见项的 Tab 自动隐藏。
544
+ * 注意 default 必须写显式完整字面量:Zod v4 的 .default(x) 在输入缺失时
545
+ * 直接取 x、不做二次 parse,内层字段默认值不会补齐。
546
+ */
547
+ displaySettings: z
548
+ .object({
549
+ /** 总开关:false(默认)不渲染齿轮入口与面板,维持现有独立按钮 */
550
+ enable: z.boolean().default(false),
551
+ /** 外观 Tab:主题色(迁入现有 hue 调色板) */
552
+ themeColorSwitchable: z.boolean().default(true),
553
+ /** 壁纸 Tab:壁纸模式四选一 */
554
+ wallpaperModeSwitchable: z.boolean().default(true),
555
+ /** 壁纸 Tab:全屏布局(经典/Hero),仅 fullscreen 模式下显示 */
556
+ fullscreenLayoutSwitchable: z.boolean().default(true),
557
+ /** 壁纸 Tab:首页壁纸标题开关(需 homeText.enable 且有文案) */
558
+ bannerTitleSwitchable: z.boolean().default(true),
559
+ /** 壁纸 Tab:壁纸轮播开关(需 carousel.enable,访客只能关不能开) */
560
+ bannerCarouselSwitchable: z.boolean().default(true),
561
+ /** 壁纸 Tab:水波纹动画开关(需 waves 配置开启) */
562
+ wavesSwitchable: z.boolean().default(true),
563
+ /** 壁纸 Tab:渐变过渡开关(需 gradient 配置开启) */
564
+ gradientSwitchable: z.boolean().default(true),
565
+ /**
566
+ * 壁纸 Tab:透明设置滑块组。布尔 = 总开关;对象分项控制。
567
+ * opacity 滑块仅 overlay 模式显示;blur/cardOpacity 在 overlay
568
+ * 与 fullscreen-hero 布局下都生效。
569
+ */
570
+ overlaySwitchable: z
571
+ .union([
572
+ z.boolean(),
573
+ z.object({
574
+ opacity: z.boolean().default(true),
575
+ blur: z.boolean().default(true),
576
+ cardOpacity: z.boolean().default(true),
577
+ }),
578
+ ])
579
+ .default({ opacity: true, blur: true, cardOpacity: true }),
580
+ /** 特效 Tab:樱花特效开关(需 sakura.enabled) */
581
+ sakuraSwitchable: z.boolean().default(true),
582
+ })
583
+ .default({
584
+ enable: false,
585
+ themeColorSwitchable: true,
586
+ wallpaperModeSwitchable: true,
587
+ fullscreenLayoutSwitchable: true,
588
+ bannerTitleSwitchable: true,
589
+ bannerCarouselSwitchable: true,
590
+ wavesSwitchable: true,
591
+ gradientSwitchable: true,
592
+ overlaySwitchable: { opacity: true, blur: true, cardOpacity: true },
593
+ sakuraSwitchable: true,
594
+ }),
292
595
  /**
293
596
  * 代码高亮(§4.61):Expressive Code(Astro 生态标准方案)接管代码块渲染,
294
597
  * 替代 §4.17 自研 shiki 管线与 §4.37 自研折叠。light/dark 为 EC 支持的 Shiki
@@ -416,7 +719,7 @@ export const themeConfigSchema = z.object({
416
719
  z.object({
417
720
  title: z.string(),
418
721
  site: z.string().url(),
419
- /** 头像图(外链或 public 路径;留空用字头像) */
722
+ /** 头像图(三格式图源 §4.97;留空用字头像) */
420
723
  image: z.string().default(""),
421
724
  desc: z.string().default(""),
422
725
  tags: z.array(z.string()).default([]),
@@ -439,7 +742,7 @@ export const themeConfigSchema = z.object({
439
742
  /**
440
743
  * 侧栏 widget 体系(P2 侧栏改造):
441
744
  * - widgets 数组即渲染顺序,可自由增删/排序/开关;类型:
442
- * announcement / categories / tags / friends / calendar / toc / siteInfo / advertisement / profile
745
+ * announcement / categories / tags / friends / calendar / toc / siteInfo / advertisement / profile / music
443
746
  * - 公共配置(开关/显隐/位置)配置在数组项上;组件专属参数统一收进 item 的
444
747
  * specificConfig 入口(§4.52,对齐参考项目):tags.specificConfig.count、
445
748
  * friends.specificConfig.count/random、calendar.specificConfig.postTooltip
@@ -467,7 +770,7 @@ export const themeConfigSchema = z.object({
467
770
  type: z.literal("tags"),
468
771
  specificConfig: z
469
772
  .object({ count: z.number().int().min(0).default(12) })
470
- .default({}),
773
+ .default({ count: 12 }),
471
774
  }),
472
775
  /**
473
776
  * 友链 widget:specificConfig.count 展示数量;random=true 构建期随机取样,
@@ -480,7 +783,7 @@ export const themeConfigSchema = z.object({
480
783
  count: z.number().int().min(1).default(3),
481
784
  random: z.boolean().default(false),
482
785
  })
483
- .default({}),
786
+ .default({ count: 3, random: false }),
484
787
  }),
485
788
  sidebarWidgetBase.extend({
486
789
  type: z.literal("calendar"),
@@ -489,9 +792,14 @@ export const themeConfigSchema = z.object({
489
792
  /** 日历下方文章列表条目悬停显示全文 title(§4.41);默认关 */
490
793
  postTooltip: z.boolean().default(false),
491
794
  })
492
- .default({}),
795
+ .default({ postTooltip: false }),
493
796
  }),
494
797
  sidebarWidgetBase.extend({ type: z.literal("toc") }),
798
+ /**
799
+ * 音乐 widget(§4.100):与 MusicPlayer 共用 config.music 歌单,
800
+ * 播放器未启用或歌单为空时不渲染;播放状态经 CustomEvent 联动
801
+ */
802
+ sidebarWidgetBase.extend({ type: z.literal("music") }),
495
803
  sidebarWidgetBase.extend({ type: z.literal("siteInfo") }),
496
804
  sidebarWidgetBase.extend({ type: z.literal("advertisement") }),
497
805
  sidebarWidgetBase.extend({ type: z.literal("profile") }),
@@ -500,9 +808,20 @@ export const themeConfigSchema = z.object({
500
808
  .default([
501
809
  { type: "announcement" },
502
810
  { type: "categories", position: "sticky" },
503
- { type: "tags", position: "sticky" },
504
- { type: "friends" },
505
- { type: "calendar", hideOnMobile: true, position: "sticky" },
811
+ // 注意:schema default 值不会二次 parse(default 原样返回),
812
+ // 因此数组项的 specificConfig 必须显式写全,运行时才有值
813
+ {
814
+ type: "tags",
815
+ position: "sticky",
816
+ specificConfig: { count: 12 },
817
+ },
818
+ { type: "friends", specificConfig: { count: 3, random: false } },
819
+ {
820
+ type: "calendar",
821
+ hideOnMobile: true,
822
+ position: "sticky",
823
+ specificConfig: { postTooltip: false },
824
+ },
506
825
  {
507
826
  type: "toc",
508
827
  postOnly: true,
@@ -518,9 +837,14 @@ export const themeConfigSchema = z.object({
518
837
  widgets: [
519
838
  { type: "announcement" },
520
839
  { type: "categories", position: "sticky" },
521
- { type: "tags", position: "sticky" },
522
- { type: "friends" },
523
- { type: "calendar", hideOnMobile: true, position: "sticky" },
840
+ { type: "tags", position: "sticky", specificConfig: { count: 12 } },
841
+ { type: "friends", specificConfig: { count: 3, random: false } },
842
+ {
843
+ type: "calendar",
844
+ hideOnMobile: true,
845
+ position: "sticky",
846
+ specificConfig: { postTooltip: false },
847
+ },
524
848
  { type: "toc", postOnly: true, hideOnMobile: true, position: "sticky" },
525
849
  { type: "siteInfo" },
526
850
  { type: "advertisement" },
@@ -542,7 +866,7 @@ export const themeConfigSchema = z.object({
542
866
  name: z.string(),
543
867
  /** 文本/emoji 角标(主题不内置图标依赖) */
544
868
  icon: z.string().default(""),
545
- /** 收款码图片(public 路径或外链) */
869
+ /** 收款码图片(三格式图源 §4.97) */
546
870
  image: z.string().default(""),
547
871
  /** 跳转链接(爱发电/ko-fi 等) */
548
872
  link: z.string().default(""),
@@ -556,6 +880,7 @@ export const themeConfigSchema = z.object({
556
880
  .array(
557
881
  z.object({
558
882
  name: z.string(),
883
+ /** 头像图(三格式图源 §4.97;留空用字头像) */
559
884
  avatar: z.string().default(""),
560
885
  amount: z.string().default(""),
561
886
  date: z.string().default(""),
@@ -727,7 +1052,7 @@ export const themeConfigSchema = z.object({
727
1052
  .object({
728
1053
  enabled: z.boolean().default(false),
729
1054
  title: z.string().default("广告"),
730
- /** 广告图(public 路径或外链,留空跳过) */
1055
+ /** 广告图(三格式图源 §4.97,留空跳过) */
731
1056
  image: z.string().default(""),
732
1057
  /** 点击广告图跳转的链接(留空则图不可点) */
733
1058
  imageLink: z.string().default(""),
@@ -760,7 +1085,7 @@ export const themeConfigSchema = z.object({
760
1085
  displayCount: -1,
761
1086
  }),
762
1087
  /**
763
- * 站点统计(P1-16):GA / Clarity / Umami / 51la 四选多。
1088
+ * 站点统计(P1-16):GA / Clarity / Umami / 51la 四选多,另配 Web Vitals 通用上报。
764
1089
  * 对应 ID 留空即不注入该统计脚本,全部留空零副作用。
765
1090
  */
766
1091
  analytics: z
@@ -832,6 +1157,19 @@ export const themeConfigSchema = z.object({
832
1157
  hashMode: false,
833
1158
  screenRecord: true,
834
1159
  }),
1160
+ /** Web Vitals 通用上报(§4.91):与统计厂商无关,POST JSON 到自建端点 */
1161
+ webVitals: z
1162
+ .object({
1163
+ /** 上报端点(载荷以 text/plain 载 JSON 发送,免 CORS 预检;留空不采集) */
1164
+ endpoint: z.string().default(""),
1165
+ /** 站点标识,随载荷一起上报(多站共用一个端点时区分来源) */
1166
+ siteId: z.string().default(""),
1167
+ /** 采样率 0~1,默认全量 */
1168
+ sampleRate: z.number().min(0).max(1).default(1),
1169
+ /** 控制台打印上报载荷(调试用) */
1170
+ debug: z.boolean().default(false),
1171
+ })
1172
+ .default({ endpoint: "", siteId: "", sampleRate: 1, debug: false }),
835
1173
  })
836
1174
  .default({
837
1175
  googleAnalyticsId: "",
@@ -858,6 +1196,7 @@ export const themeConfigSchema = z.object({
858
1196
  hashMode: false,
859
1197
  screenRecord: true,
860
1198
  },
1199
+ webVitals: { endpoint: "", siteId: "", sampleRate: 1, debug: false },
861
1200
  }),
862
1201
  /* ---------- LaTeX 数学公式(M3b-b4) ---------- */
863
1202
  latex: z
@@ -941,6 +1280,7 @@ export const themeConfigSchema = z.object({
941
1280
  /**
942
1281
  * favicon 图标列表(§4.30):数组非空时替换默认 /favicon.svg;
943
1282
  * theme 指定 "light"/"dark" 时按系统深浅色经 media 切换(可配双图标)。
1283
+ * src 仅支持 public 路径 / 外链(不参与 src/assets 三格式解析,ico 无优化需求)。
944
1284
  */
945
1285
  favicon: z
946
1286
  .array(
@@ -970,13 +1310,74 @@ export const themeConfigSchema = z.object({
970
1310
  * 樱花飘落特效(§4.30):全站 canvas 粒子(程序化花瓣,零贴图资产),
971
1311
  * prefers-reduced-motion 自动停用,页签隐藏暂停。
972
1312
  */
1313
+ /**
1314
+ * 樱花飘落特效(§4.30 / §4.101 八维参数对齐 Firefly):全站 canvas 粒子
1315
+ * (程序化花瓣,零贴图资产),prefers-reduced-motion 自动停用,页签隐藏暂停。
1316
+ * 注意:外层 default 必须全量展开嵌套值——zod 的 default 原样返回不二次 parse,
1317
+ * 写 `.default({})` 会导致深层字段运行时缺失。
1318
+ */
973
1319
  sakura: z
974
1320
  .object({
975
1321
  enabled: z.boolean().default(false),
976
1322
  /** 同屏花瓣数 */
977
1323
  density: z.number().int().min(1).max(200).default(24),
1324
+ /** 飘落轮数:-1 无限循环;>=1 时落满轮数后不再重生,花瓣清空即停止动画 */
1325
+ limitTimes: z.number().int().min(-1).default(-1),
1326
+ /** canvas 层级(z-index),默认 100(低于 Header/悬浮球) */
1327
+ zIndex: z.number().int().default(100),
1328
+ /** 花瓣尺寸倍数区间(相对基准尺寸随机取值) */
1329
+ size: z
1330
+ .object({
1331
+ min: z.number().min(0.1).default(0.5),
1332
+ max: z.number().min(0.1).default(1.1),
1333
+ })
1334
+ .default({ min: 0.5, max: 1.1 }),
1335
+ /** 花瓣透明度区间 */
1336
+ opacity: z
1337
+ .object({
1338
+ min: z.number().min(0).max(1).default(0.3),
1339
+ max: z.number().min(0).max(1).default(0.9),
1340
+ })
1341
+ .default({ min: 0.3, max: 0.9 }),
1342
+ /** 运动速度:horizontal 水平漂移 px/帧(负值向左)、vertical 下落 px/帧、rotation 每帧自转弧度、fadeSpeed 临终淡出速度(不应大于 opacity.min) */
1343
+ speed: z
1344
+ .object({
1345
+ horizontal: z
1346
+ .object({
1347
+ min: z.number().default(-1.7),
1348
+ max: z.number().default(-1.2),
1349
+ })
1350
+ .default({ min: -1.7, max: -1.2 }),
1351
+ vertical: z
1352
+ .object({
1353
+ min: z.number().default(1.5),
1354
+ max: z.number().default(2.2),
1355
+ })
1356
+ .default({ min: 1.5, max: 2.2 }),
1357
+ rotation: z.number().default(0.03),
1358
+ fadeSpeed: z.number().default(0.03),
1359
+ })
1360
+ .default({
1361
+ horizontal: { min: -1.7, max: -1.2 },
1362
+ vertical: { min: 1.5, max: 2.2 },
1363
+ rotation: 0.03,
1364
+ fadeSpeed: 0.03,
1365
+ }),
978
1366
  })
979
- .default({ enabled: false, density: 24 }),
1367
+ .default({
1368
+ enabled: false,
1369
+ density: 24,
1370
+ limitTimes: -1,
1371
+ zIndex: 100,
1372
+ size: { min: 0.5, max: 1.1 },
1373
+ opacity: { min: 0.3, max: 0.9 },
1374
+ speed: {
1375
+ horizontal: { min: -1.7, max: -1.2 },
1376
+ vertical: { min: 1.5, max: 2.2 },
1377
+ rotation: 0.03,
1378
+ fadeSpeed: 0.03,
1379
+ },
1380
+ }),
980
1381
  /**
981
1382
  * 音乐播放器(§4.36):local 模式,自研播放内核(audio + LRC 解析),
982
1383
  * 根节点 transition:persist 跨页不断播。meting 在线模式预留后续扩展。
@@ -1001,7 +1402,7 @@ export const themeConfigSchema = z.object({
1001
1402
  artist: z.string().default(""),
1002
1403
  /** 音频地址(public 路径或外链) */
1003
1404
  url: z.string(),
1004
- /** 封面图(可选,缺省渲染唱片图标) */
1405
+ /** 封面图(三格式图源 §4.97,可选,缺省渲染唱片图标) */
1005
1406
  cover: z.string().optional(),
1006
1407
  /** 歌词:.lrc 文件路径或内联 LRC 文本(可选,缺省无歌词) */
1007
1408
  lrc: z.string().optional(),